(Documentation) |
(Adding weapons to Category:Hit-Scan Weapons and Category:Projectile Weapons) |
||
(801 intermediate revisions by 9 users not shown) | |||
Line 1: | Line 1: | ||
− | --- |
+ | --- '''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 |
||
− | -- @attribution [[User:Calenhed|Calenhed]] |
||
+ | -- @require [[Module:StatObject]] |
||
− | -- @image |
||
− | -- |
+ | -- @require [[Module:DamageTypes]] |
− | -- |
+ | -- @require [[Module:Polarity]] |
− | -- |
+ | -- @require [[Module:Math]] |
-- @require [[Module:Table]] |
-- @require [[Module:Table]] |
||
− | -- |
+ | -- @require [[Module:Tooltips]] |
− | -- |
+ | -- @require [[Module:Version]] |
− | -- |
+ | -- @require [[Module:Stances/data]] |
− | -- |
+ | -- @require [[Module:Weapons/data]] |
− | -- |
+ | -- @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 |
+ | local Delay = require([[Module:Delay]]) |
− | local |
+ | local WeaponData = Delay.require([[Module:Weapons/data]]) |
− | local |
+ | local WarframeData = Delay.require([[Module:Warframes/data]]) -- for use in p.getListWithWarframes |
− | local |
+ | local ConclaveData = Delay.require([[Module:Weapons/Conclave/data]]) |
− | local |
+ | local Tooltip = Delay.require([[Module:Tooltips]]) -- full, icon |
+ | local Version = Delay.require([[Module:Version]]) -- _getVersion, _getVersionDate |
||
− | |||
− | local |
+ | local Polarity = Delay.require([[Module:Polarity]]) -- _pols, _polarity |
− | local Math = require |
+ | local Math = Delay.require([[Module:Math]]) -- formatnum |
− | local |
+ | local Table = Delay.require([[Module:Table]]) -- size, skpairs |
− | local |
+ | local iterationOrderArray = require([[Module:DamageTypes]]).iterationOrderArray |
− | 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" |
||
− | } |
||
+ | -- TODO: Should decouple from localized names for internationalization |
||
local VARIANT_LIST = { |
local VARIANT_LIST = { |
||
− | "Prime", "Prisma", "Wraith", "Vandal", "Vaykor", "Synoid", |
+ | "Prime", "Prisma", "Wraith", "Vandal", "Vaykor", "Synoid", "Telos", "Secura", |
− | " |
+ | "Sancti", "Rakta", "Mara", "Carmine", "Ceti", "Dex", "MK1", "Kuva", "Tenet" |
} |
} |
||
+ | table.unpack = table.unpack or unpack |
||
− | local function makeDTooltip(dt, addText) |
||
+ | |||
− | local text = (addText) and dt or '' |
||
+ | local StatObject = require [[Module:StatObject]] |
||
− | return ('<span class="tooltip" data-param="%s" data-param2="DamageTypes">[[File:Dmg%sSmall64.png|x19px|link=]] %s</span>') |
||
+ | p.__StatObject = StatObject |
||
− | :format(dt, dt, text) |
||
+ | 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 |
end |
||
+ | p._getAttack = getWeaponAttack |
||
+ | p._getWeaponAttack = getWeaponAttack |
||
− | + | function p._statRead(w, a, ...) |
|
+ | return statRead(getWeaponAttack(w, a), ...) |
||
− | if (attackEntry.Damage ~= nil and Table.size(attackEntry.Damage) > 0) then |
||
+ | end |
||
− | local totalDmg = 0 |
||
+ | function p._statFormat(w, a, ...) |
||
− | local bestDmg = 0 |
||
+ | return statFormat(getWeaponAttack(w, a), ...) |
||
− | local bestElement = nil |
||
+ | end |
||
− | local elemCount = 0 |
||
+ | function p.stat(frame) |
||
− | for damageType, dmg in pairs(attackEntry.Damage) do |
||
+ | return p._statFormat(p._getWeapon(frame.args[1] or 'Skana Prime'), nil, frame.args[2] or 'Name') |
||
− | if (dmg > bestDmg) then |
||
+ | end |
||
− | bestDmg = dmg |
||
+ | |||
− | bestElement = damageType |
||
+ | -- Wrapper function for use in StatObject |
||
− | end |
||
+ | local function dmgTooltip(damageType) |
||
− | totalDmg = totalDmg + dmg |
||
+ | return Tooltip.full(damageType, 'DamageTypes') |
||
− | if (dmg > 0) then |
||
+ | end |
||
− | elemCount = elemCount + 1 |
||
+ | |||
+ | -- 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 |
end |
||
+ | total = total + dmg |
||
end |
end |
||
+ | return StatObject.ucacheIn(weapAtk, 'DamageBias', { bestdmg / total, bestdt, total }) |
||
− | -- Make sure there are two damage instances that are above zero |
||
+ | end, |
||
− | -- Exception for physical damage types |
||
+ | { percent, passes(dmgTooltip), '' } |
||
− | if (elemCount > 0) then |
||
+ | }, |
||
− | return (bestDmg / totalDmg), bestElement |
||
+ | 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 |
end |
||
+ | return ('%.2f°'):format(value) |
||
− | error('getDamageBias(Attack): '.. |
||
+ | end |
||
− | 'Damage key in Attack entry has no key-value pairs'..mw.dumpObject(attackEntry)) |
||
+ | }, |
||
+ | -- 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°' }, |
||
+ | 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 |
end |
||
+ | return Math.formatnum(dmg) |
||
− | error('getDamageBias(Attack): Attack entry has no Damage key') |
||
end |
end |
||
+ | }, |
||
− | |||
+ | SlamRadialElement = { nil, function(self, value) |
||
− | --- Returns the highest damage type of an attack in the form of a percentage |
||
+ | return value ~= nil and Tooltip.full(value, 'DamageTypes') or 'Same damage type distribution as normal attack' |
||
− | -- followed by a tooltip icon of the damage type |
||
− | -- @function getDamageBiasString |
||
− | -- @param {table} attackEntry Attack table |
||
− | -- @returns {string} Resultant text |
||
− | local function getDamageBiasString(attackEntry) |
||
− | local bestPercent, bestElement = getDamageBias(attackEntry) |
||
− | local result = Math.percentage(bestPercent) |
||
− | return result..' '..makeDTooltip(bestElement) |
||
end |
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 |
||
− | --- Calculates the derived damage stats commonly used in comparing gun performance. |
||
+ | on a single player input for a particular attack. |
||
− | -- Using what attacks the weapons tooltips are displaying for DPS calculations. |
||
+ | |||
− | -- @function calculateGunDerivedDamage |
||
+ | A single player input is defined as: |
||
− | -- @param {table} Weapon Weapon table data as seen in M:Weapons/data |
||
+ | * a single attack button press for semi-auto and burst trigger weapons |
||
− | -- @returns {number} Returns five number stats in a tuple: |
||
+ | * the moment the next shot is being fired when the attack button is being held for automatic/held trigger weapons |
||
− | -- * Total damage: final damage when accounting multishot; same as arsenal display |
||
+ | * the action of holding the attack button for charge trigger weapons |
||
− | -- * Average shot: damage per single input |
||
+ | * for duplex-fire trigger weapons, the hold and release of the attack button counts as two inputs |
||
− | -- * Average burst DPS: damage per second without reloading |
||
+ | |||
− | -- * Average sustained DPS: damage per second w/ reloading |
||
+ | A shot is defined as the base unit of attack of a weapon when unmodded. |
||
− | -- * Average lifetime damage: total damage that can be dealt in a single magazine + reserve ammo w/o picking up ammo drops |
||
+ | * A single attack input can launch several shots as in the case of burst-fire weapons. |
||
− | local function calculateGunDerivedDamage(Weapon) |
||
+ | * A single shot can shoot more than one projectile, affected by the multishot stat, as in the case of shotguns. |
||
− | local TooltipAttack = Weapon[Weapon['TooltipAttackDisplay'] or 'Attack1'] or {} |
||
+ | * 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). |
||
− | local totalDamage = 0 |
||
+ | |||
− | |||
+ | A gun can have multiple attacks which can be triggered using different buttons |
||
− | for damageType, value in pairs(TooltipAttack['Damage'] or {}) do |
||
+ | and/or types of button inputs (e.g. pressing vs. holding) |
||
− | if (damageType == 'MinProgenitorBonus') then |
||
+ | ]]-- |
||
− | -- For a more competitive comparison, assume that Lich weapons have |
||
+ | CalcDamage = function(weapAtk) |
||
− | -- max base damage bonus (+60% of a Progenitor Warframe's element); |
||
+ | local weapon, attack = weapAtk, weapAtk |
||
− | -- The value stored in 'MinProgenitorBonus' is based on the minimum +25% bonus |
||
+ | -- Count |
||
− | totalDamage = totalDamage + (value * 0.6/0.25) |
||
+ | -- How many shots are fired in a single player input |
||
− | else |
||
+ | local tapShots = statRead(weapAtk, 'BurstCount') |
||
− | totalDamage = totalDamage + value |
||
+ | -- 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) |
||
− | totalDamage = totalDamage * (TooltipAttack['Multishot'] or 1) |
||
+ | |||
+ | -- 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 critChance = TooltipAttack['CritChance'] |
||
+ | local avgDmgPerMag = avgDmgOnTap * magTaps |
||
− | 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'] |
||
+ | -- 1 is needed b/c one whole magazine is not included in reserve ammo count |
||
− | -- 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 |
-- If there is no reserve ammo, that means that weapon can deal an infinite amount of damage theoretically |
||
− | local avgLifetimeDmg = |
+ | local avgLifetimeDmg = (ammoMax ~= nil) and avgDmgPerMag * (1 + (ammoMax / magazine)) or math.huge |
+ | -- Damage / Time |
||
− | -- Need to ignore the first shot of guns since it is instantaneous and is |
||
+ | local baseDps = hitDamage * multishot / shotTime |
||
− | -- not affected by fire rate (which causes the delay between shots) |
||
+ | local avgSustainedDps = avgDmgPerMag / (magDepletionTime + reloadDelayTime + reloadTime) / tapShots |
||
− | local numShotPerMag = magazine / (TooltipAttack['AmmoCost'] or 1) |
||
+ | local avgBurstDps = avgDmgOnTap / (tapTime + shotDelayTime) / tapShots |
||
− | local avgBurst = avgShot * fireRate |
||
+ | -- Note that burst DPS can also be calculated as such: |
||
− | local avgSustained |
||
+ | -- local avgBurstDps = (hitDamage * avgCritMults * maxProgenitorBonus) * multishot / shotTime |
||
− | |||
+ | -- local avgBurstDps = avgDmgPerMag / magDepletionTime |
||
− | if Weapon.Name ~= 'Vectis' and Weapon.Name ~= 'Vectis Prime' then |
||
+ | |||
− | avgSustained = avgBurst * numShotPerMag / (fireRate * reloadTime + numShotPerMag) |
||
+ | return StatObject.ucacheIn(weapAtk, 'CalcDamage', |
||
− | else |
||
+ | { hitDamage, avgDmgOnTap, avgBurstDps, avgSustainedDps, avgLifetimeDmg, baseDps, avgDmgPerMag } |
||
− | avgSustained = avgBurst * numShotPerMag / (fireRate * reloadTime + numShotPerMag - 1) |
||
+ | ) |
||
+ | 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 |
end |
||
+ | return 0 |
||
− | |||
+ | end, |
||
− | return totalDamage, avgShot, avgBurst, avgSustained, avgLifetimeDmg |
||
+ | AttackSpeed = { --[[ors('AttackSpeed', 'FireRate')]]function(weapAtk) |
||
− | end |
||
+ | if not statRead(weapAtk, 'IsMelee') then |
||
− | |||
+ | error('AttackSpeed: Cannot get AttackSpeed attribute for a non-melee weapon; use p.statRead(weapAtk, "FireRate") instead') |
||
− | local ATTACK_KEY_MAP = { |
||
+ | end |
||
− | Impact = function(attackEntry) return attackEntry['Damage']['Impact'] or 0 end, |
||
+ | return statRead(weapAtk, 'FireRate') |
||
− | Puncture = function(attackEntry) return attackEntry['Damage']['Puncture'] or 0 end, |
||
+ | end, '%.2fx animation speed' |
||
− | Slash = function(attackEntry) return attackEntry['Damage']['Slash'] or 0 end, |
||
+ | }, |
||
− | Cold = function(attackEntry) return attackEntry['Damage']['Cold'] or 0 end, |
||
− | + | IsMelee = function(weapAtk) return statRead(weapAtk, 'Slot'):find('Melee') or statRead(weapAtk, 'Slot'):find('melee') ~= nil end, |
|
+ | IsSilent = ors('IsSilent', 'IsMelee', false), |
||
− | Heat = function(attackEntry) return attackEntry['Damage']['Heat'] or 0 end, |
||
+ | HasAoEAttack = function(weap) |
||
− | Toxin = function(attackEntry) return attackEntry['Damage']['Toxin'] or 0 end, |
||
− | + | for i, attackEntry in pairs(weap['Attacks']) do |
|
− | + | if attackEntry['ShotType'] == 'AoE' then |
|
+ | return true |
||
− | 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 |
end |
||
+ | end |
||
− | return attackEntry['FireRate'] |
||
+ | return false |
||
− | end, |
||
+ | end, |
||
− | BaseDamage = function(attackEntry) |
||
+ | Conclave = false, |
||
− | local total = 0 |
||
+ | Image = { 'Panel.png', '[[File:%s|link=]]' }, |
||
− | for damageType, value in pairs(attackEntry['Damage']) do |
||
+ | Attacks = ors('Attacks', p._getAttack, {}), |
||
− | total = total + value |
||
+ | 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 |
||
+ | end |
||
− | return total |
||
+ | table.sort(result, function(a,b) return a.Name<b.Name end) |
||
− | end, |
||
+ | return result |
||
− | TotalDamage = function(attackEntry) |
||
+ | end, function(self, result) |
||
− | local total = 0 |
||
− | + | for i,w in ipairs(result) do |
|
+ | result[i]=Tooltip.full(w.Name, 'Weapons', w) |
||
− | total = total + value |
||
− | + | end |
|
+ | return table.concat(result, '<br />') |
||
− | return total * (attackEntry['Multishot'] or 1) |
||
− | + | end |
|
+ | }, |
||
− | DamageBias = function(attackEntry) return getDamageBiasString(attackEntry) end, |
||
− | + | BaseName = function(weapAtk) return weapAtk['BaseName'] or ({p._getVariant(statRead(weapAtk, 'Name'))})[3] end, |
|
+ | -- TODO: Add comments to Explosion function for readability |
||
− | CritChance = function(attackEntry) return Math.percentage(attackEntry['CritChance']) end, |
||
+ | -- TODO: Do not rely on attack name to determine what AoE component is attached to which main direct hit component |
||
− | CritMultiplier = function(attackEntry) return (attackEntry['CritMultiplier'])..'x' end, |
||
+ | ---^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 |
||
− | FalloffEnd = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['EndRange'] or 600 end, |
||
+ | -- TODO: Use ShotType = "AoE" to determine if attack entry is AoE |
||
− | FalloffReduction = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['Reduction'] or 0.99 end, |
||
+ | Explosion = function(weapAtk) |
||
− | FalloffStart = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['StartRange'] or 300 end, |
||
+ | local weap, atk = weapAtk, weapAtk |
||
− | FireRate = function(attackEntry) return attackEntry['FireRate'] end, |
||
+ | -- tbh this is a mess |
||
− | HeadshotMultiplier = function(attackEntry) |
||
+ | local explosion = weapAtk['Explosion'] or statRead(weapAtk, 'AttackName'):gsub(' Impact',''):gsub(' Contact','')..' Explosion' |
||
− | return attackEntry['HeadshotMultiplier'] and (attackEntry['HeadshotMultiplier'])..'x' or '1x' |
||
+ | if type(explosion) == 'string' then |
||
− | end, |
||
+ | explosion = weap.Attacks[tonumber(explosion:gsub('%D',''))] or explosion |
||
− | Multishot = function(attackEntry) return attackEntry['Multishot'] or 1 end, |
||
+ | elseif type(explosion) == 'number' then |
||
− | PelletName = function(attackEntry) return attackEntry['PelletName'] end, |
||
+ | explosion = weap.Attacks[explosion] or explosion |
||
− | PunchThrough = function(attackEntry) return attackEntry['PunchThrough'] or 0 end, |
||
+ | end |
||
− | Radius = function(attackEntry) return attackEntry['Radius'] end, |
||
+ | local explosions = {} |
||
− | ShotSpeed = function(attackEntry) return attackEntry['ShotSpeed'] end, |
||
+ | if type(explosion) ~= 'table' then |
||
− | ShotType = function(attackEntry) return attackEntry['ShotType'] end, |
||
+ | for i, v in ipairs(weap.Attacks) do |
||
− | StatusChance = function(attackEntry) return Math.percentage(attackEntry['StatusChance']) end, |
||
+ | if p._statRead(weapAtk, v, 'AttackName'):find 'xplosion' then |
||
− | } |
||
+ | if p._statRead(weapAtk, v, 'AttackName') == explosion then |
||
− | |||
+ | explosions[1] = nil |
||
− | local SHARED_KEY_MAP = { |
||
+ | explosion = v |
||
− | Class = function(weaponEntry) return weaponEntry['Class'] end, |
||
+ | break |
||
− | Disposition = function(weaponEntry) |
||
+ | end |
||
− | return weaponEntry['Disposition'] ~= nil and weaponEntry['Disposition'] or 'N/A' |
||
+ | table.insert(explosions, v) |
||
− | end, |
||
− | Family = function(weaponEntry) return weaponEntry['Family'] end, |
||
− | Introduced = function(weaponEntry) |
||
− | return weaponEntry['Introduced'] and Version._getVersion(weaponEntry['Introduced'])['Name'] or 'N/A' |
||
− | end, |
||
− | IntroducedDate = function(weaponEntry) |
||
− | return weaponEntry['Introduced'] and Version._getVersionDate(weaponEntry['Introduced']) or 'N/A' |
||
− | end, |
||
− | Link = function(weaponEntry) return '[['..weaponEntry['Link']..']]' end, |
||
− | Mastery = function(weaponEntry) return weaponEntry['Mastery'] or 'N/A' end, |
||
− | Name = function(weaponEntry) return '[['..weaponEntry['Name']..']]' end, |
||
− | NameLink = function(weaponEntry) return '[['..weaponEntry['Link']..'|'..weaponEntry['Name']..']]' end, |
||
− | Polarities = function(weaponEntry) return Polarity._pols(weaponEntry['Polarities']) end, |
||
− | Traits = function(weaponEntry) |
||
− | local traitString = {} |
||
− | for _, trait in ipairs(weaponEntry['Traits']) do |
||
− | table.insert(traitString, trait) |
||
− | end |
||
− | return table.concat(traitString, ', ') |
||
− | end, |
||
− | Type = function(weaponEntry) return weaponEntry['Type'] end |
||
− | } |
||
− | |||
− | 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 |
||
end |
end |
||
+ | explosion = explosions[1] or explosion |
||
− | return '' |
||
− | end |
+ | end |
+ | StatObject.pcacheIn(getWeaponAttack(weap, explosion), 'BaseAttack', atk) |
||
− | AvgProcCount = function(weaponEntry) |
||
+ | return StatObject.pucacheIn(weapAtk, 'Explosion', explosion) |
||
− | local attackEntry = weaponEntry[weaponEntry['TooltipAttackDisplay'] or 'Attack1'] |
||
+ | end, |
||
− | local statusChance = attackEntry['StatusChance'] or 0 |
||
+ | IsVariant = function(weap) |
||
− | local multishot = attackEntry['Multishot'] or 1 |
||
+ | return StatObject.pucacheIn(weap, 'IsVariant', p._isVariant(statRead(weap, 'Name'))) |
||
− | local numForcedProcs = attackEntry['ForcedProcs'] and Table.size(attackEntry['ForcedProcs']) or 0 |
||
+ | end, |
||
− | return (statusChance + numForcedProcs) * multishot |
||
+ | Variant = indexes('IsVariant', 2), |
||
− | end, |
||
+ | BaseName = indexes('IsVariant', 3), |
||
− | AvgProcPerSec = function(weaponEntry) |
||
+ | Categories = { function(weapAtk) |
||
− | local attackEntry = weaponEntry[weaponEntry['TooltipAttackDisplay'] or 'Attack1'] |
||
− | + | local cats = { 'Weapons' } |
|
+ | -- Adding editor-defined traits from M:Weapons/data |
||
− | local multishot = attackEntry['Multishot'] or 1 |
||
+ | -- Note to make sure they have a proper category page associated with a trait |
||
− | local numForcedProcs = attackEntry['ForcedProcs'] and Table.size(attackEntry['ForcedProcs']) or 0 |
||
+ | for _, trait in ipairs(weapAtk.Traits or {}) do |
||
− | local fireRate = (attackEntry['ChargeTime'] ~= nil) and (1 / attackEntry['ChargeTime']) or attackEntry['FireRate'] |
||
+ | table.insert(cats, trait..' Weapons') |
||
− | return (statusChance + numForcedProcs) * multishot * fireRate |
||
− | end |
+ | end |
+ | --[=[ |
||
− | AvgShotDmg = function(weaponEntry) |
||
− | local totalDamage, avgShot = calculateGunDerivedDamage(weaponEntry) |
||
− | return avgShot |
||
− | end, |
||
− | BurstDps = function(weaponEntry) |
||
− | local totalDamage, avgShot, avgBurst = calculateGunDerivedDamage(weaponEntry) |
||
− | return avgBurst |
||
− | end, |
||
− | ExilusPolarity = function(weaponEntry) return Polarity._polarity(weaponEntry['ExilusPolarity']) end, |
||
− | IsSilent = function(weaponEntry) return weaponEntry['IsSilent'] end, |
||
− | Magazine = function(weaponEntry) return weaponEntry['Magazine'] end, |
||
− | MaxAmmo = function(weaponEntry) return weaponEntry['MaxAmmo'] end, |
||
− | Range = function(weaponEntry) return weaponEntry['Range'] end, |
||
− | Reload = function(weaponEntry) return weaponEntry['Reload'] end, |
||
− | ReloadDelay = function(weaponEntry) return weaponEntry['ReloadDelay'] or 0 end, |
||
− | ReloadDelayEmpty = function(weaponEntry) return weaponEntry['ReloadDelayEmpty'] or 0 end, |
||
− | ReloadStyle = function(weaponEntry) return weaponEntry['ReloadStyle'] end, |
||
− | Spool = function(weaponEntry) return weaponEntry['Spool'] or 0 end, |
||
− | SustainedDps = function(weaponEntry) |
||
− | local totalDamage, avgShot, avgBurst, avgSustained = calculateGunDerivedDamage(weaponEntry) |
||
− | return avgSustained |
||
− | end, |
||
− | Trigger = function(weaponEntry) return weaponEntry['Trigger'] end |
||
− | } |
||
− | |||
− | local MELEE_KEY_MAP = { |
||
− | BlockAngle = function(weaponEntry) return weaponEntry['BlockAngle'] or 0 end, |
||
− | ComboDur = function(weaponEntry) return weaponEntry['ComboDur'] or 0 end, |
||
− | FollowThrough = function(weaponEntry) return weaponEntry['FollowThrough'] or 0 end, |
||
− | HeavyAttack = function(weaponEntry) return weaponEntry['HeavyAttack'] or 0 end, |
||
− | HeavyElement = function(weaponEntry) return weaponEntry['HeavyElement'] end, |
||
− | HeavySlamAttack = function(weaponEntry) return weaponEntry['HeavySlamAttack'] end, |
||
− | HeavySlamElement = function(weaponEntry) return weaponEntry['HeavySlamElement'] end, |
||
− | HeavyRadialDmg = function(weaponEntry) return weaponEntry['HeavyRadialDmg'] or 0 end, |
||
− | HeavyRadialElement = function(weaponEntry) return weaponEntry['HeavyRadialElement'] end, |
||
− | HeavySlamRadius = function(weaponEntry) return weaponEntry['HeavySlamRadius'] or 0 end, |
||
− | MeleeRange = function(weaponEntry) return weaponEntry['MeleeRange'] or 0 end, |
||
− | SlamAttack = function(weaponEntry) return weaponEntry['SlamAttack'] or 0 end, |
||
− | SlamElement = function(weaponEntry) return weaponEntry['SlamElement'] end, |
||
− | SlamRadialDmg = function(weaponEntry) return weaponEntry['SlamRadialDmg'] or 0 end, |
||
− | SlamRadialElement = function(weaponEntry) return weaponEntry['SlamRadialElement'] end, |
||
− | SlamRadialProc = function(weaponEntry) return weaponEntry['SlamRadialProc'] end, |
||
− | SlamRadius = function(weaponEntry) return weaponEntry['SlamRadius'] or 0 end, |
||
− | SlideAttack = function(weaponEntry) return weaponEntry['SlideAttack'] or 0 end, |
||
− | SlideElement = function(weaponEntry) return weaponEntry['SlideElement'] end, |
||
− | Stances = function(weaponEntry) return getWeaponStanceList(weaponEntry) end, |
||
− | StancePolarity = function(weaponEntry) |
||
− | return weaponEntry['StancePolarity'] ~= nil and |
||
− | Polarity._polarity(weaponEntry['StancePolarity']) or 'N/A' |
||
− | end, |
||
− | WindUp = function(weaponEntry) return weaponEntry['WindUp'] or 0 end |
||
− | } |
||
− | |||
− | -- For mapping weapon traits or types to a category link |
||
local CATEGORY_MAP = { |
local CATEGORY_MAP = { |
||
− | Primary = ' |
+ | Primary = 'Primary Weapons', |
− | Secondary = ' |
+ | Secondary = 'Secondary Weapons', |
− | Melee = ' |
+ | Melee = 'Melee Weapons', |
− | [' |
+ | ['Archmelee'] = 'Archwing Melee', |
− | [' |
+ | ['Archgun'] = 'Archwing Gun', |
− | [' |
+ | ['Archgun (Atmosphere)'] = 'Archwing Gun', |
− | Kitgun = ' |
+ | Kitgun = 'Kitgun', |
− | Zaw = ' |
+ | Zaw = 'Zaw', |
− | ['Railjack |
+ | ['Railjack Turret'] = 'Railjack', |
+ | ['Railjack Armament'] = 'Railjack', |
||
− | Gear = '[[Category:Gear]]', |
||
+ | Gear = 'Gear', |
||
− | Rifle = ' |
+ | Rifle = 'Assault Rifle', |
− | ['Sniper Rifle'] = ' |
+ | ['Sniper Rifle'] = 'Sniper Rifle', |
− | Shotgun = ' |
+ | Shotgun = 'Shotgun', |
− | Pistol = ' |
+ | Pistol = 'Pistol', |
− | ['Dual Pistols'] = ' |
+ | ['Dual Pistols'] = 'Dual Pistols', |
− | Bow = ' |
+ | Bow = 'Bow', |
− | Launcher = ' |
+ | Launcher = 'Launcher', |
− | ['Arm-Cannon'] = ' |
+ | ['Arm-Cannon'] = 'Arm-Cannon', |
− | ['Speargun'] = ' |
+ | ['Speargun'] = 'Speargun', |
− | Thrown = ' |
+ | Thrown = 'Thrown', |
− | ['Shotgun Sidearm'] = ' |
+ | ['Shotgun Sidearm'] = 'Shotgun Sidearm', |
− | Prime = ' |
+ | Prime = 'Prime Weapons', |
− | ['Never Vaulted'] = ' |
+ | ['Never Vaulted'] = 'Never Vaulted', |
− | Vaulted = ' |
+ | Vaulted = 'Vaulted', |
− | Wraith = ' |
+ | Wraith = 'Wraith', |
− | Vandal = ' |
+ | Vandal = 'Vandal', |
+ | ['Kuva Lich'] = 'Kuva Lich', |
||
− | -- Maybe replace with 'Kuva' category? Though technically Broken Scepter is a "Kuva" weapon which |
||
+ | ['Kuva Lich'] = 'Kuva', |
||
− | -- is why this distinction is made (in the past, editors mislabeled this trait) |
||
+ | Prisma = 'Prisma', |
||
− | ['Kuva Lich'] = '[[Category:Kuva Lich]]', |
||
− | Prisma = '[[Category:Prisma]]', |
||
− | Grineer = ' |
+ | Grineer = 'Grineer Weapons', |
− | Corpus = ' |
+ | Corpus = 'Corpus Weapons', |
− | Infested = ' |
+ | Infested = 'Infested Weapons', |
− | Tenno = ' |
+ | Tenno = 'Tenno Weapons', |
− | Sentient = ' |
+ | Sentient = 'Sentient Weapons', |
− | Entrati = ' |
+ | Entrati = 'Entrati Weapons', |
− | Baro = ' |
+ | Baro = 'Baro Ki\'Teer Offering', |
− | Syndicate = ' |
+ | Syndicate = 'Syndicate Offerings', |
− | ['Invasion Reward'] = ' |
+ | ['Invasion Reward'] = 'Invasion Reward', |
− | ['Alt Fire'] = ' |
+ | ['Alt Fire'] = 'Weapons with Alt Fire', |
− | ['AoE'] = ' |
+ | ['AoE'] = '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]]' |
||
} |
} |
||
+ | --]=] |
||
+ | 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") |
||
− | --- Builds a CSV table of all WARFRAME's guns with the exception of Kitguns. |
||
+ | table.insert(cats, family) |
||
− | -- @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' |
||
− | } |
||
+ | local slot = p._getValue(weapAtk, "Slot") |
||
− | for i, _ in ipairs(tableHeader) do table.insert(tableEntryTemplate, '%s,') end |
||
+ | table.insert(cats, slot..' Weapons') |
||
− | tableEntryTemplate[#tableEntryTemplate] = '%s' -- Last column |
||
− | tableEntryTemplate = table.concat(tableEntryTemplate) |
||
+ | -- TODO: Move all these trigger categories to be in the format "Category:Trigger Name Weapons" |
||
− | local csvResult = { '<pre>' } |
||
+ | local trigger = p._getValue(weapAtk, "Trigger") |
||
− | table.insert(csvResult, string.format(tableEntryTemplate, unpack(tableHeader))) |
||
+ | 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 weaponName, weaponData in Table.skpairs(WeaponData['Weapons']) do |
||
+ | for _, user in ipairs(users) do table.insert(cats, user) end |
||
− | if (Table.contains(weaponTypesFilter, weaponData['Type'])) then |
||
+ | |||
− | -- Going through all the possible Attack keys and adding them to CSV |
||
+ | local variant = p._getValue(weapAtk, "Variant") |
||
− | -- (TODO: consolidate these keys into one Attack table with key-value pairs) |
||
+ | table.insert(cats, variant) |
||
− | for i = 1, 9, 1 do |
||
+ | |||
− | if (weaponData['Attack'..i] ~= nil) then |
||
+ | local infAmmo = p._getValue(weapAtk, "AmmoMax") == math.huge |
||
− | local totalDamage, avgShot, avgBurst, avgSustained, avgLifetimeDmg = calculateGunDerivedDamage(weaponData) |
||
+ | local accuracy = p._getValue(weapAtk, "Accuracy") |
||
− | local weaponAttack = weaponData['Attack'..i] |
||
+ | local pinpoint = accuracy ~= nil and accuracy >= 100 |
||
− | |||
+ | local regens = p._getValue(weapAtk, "ReloadRate") > 0 |
||
− | local baseDamage = 0 |
||
+ | local silent = weapAtk.IsSilent -- automatically includes |
||
− | for damageType, damageValue in pairs(weaponAttack['Damage']) do |
||
+ | local single = p._getValue(weapAtk, "Magazine") == 1 and not p._getValue(weapAtk, "IsMelee")--meh, delet? |
||
− | baseDamage = baseDamage + damageValue |
||
+ | local spools = p._getValue(weapAtk, "Spool") > 0 |
||
− | end |
||
+ | |||
− | |||
+ | local isHitScan = p._getValue(weapAtk, "ShotType") == "Hit-Scan" |
||
− | local tableEntryValues = { |
||
+ | local isProjectile = p._getValue(weapAtk, "ShotType") == "Projectile" |
||
− | weaponName, |
||
+ | local isAoE = p._getValue(weapAtk, "HasAoEAttack") |
||
− | tostring(weaponData['Trigger']), |
||
+ | local isCodexSecret = p._getValue(weapAtk, "CodexSecret") |
||
− | tostring(weaponAttack['AttackName'] or 'Normal'), |
||
+ | local isTradable = p._getValue(weapAtk, "Tradable") |
||
− | tostring(weaponAttack['Damage']['Impact'] or 0), |
||
+ | local isInConclave = p._getValue(weapAtk, "Conclave") |
||
− | tostring(weaponAttack['Damage']['Puncture'] or 0), |
||
+ | |||
− | tostring(weaponAttack['Damage']['Slash'] or 0), |
||
+ | -- Arbitrarily ordering misc categories |
||
− | tostring(weaponAttack['Damage']['Cold'] or 0), |
||
+ | if infAmmo then table.insert(cats, 'Infinite Ammo Weapons') end |
||
− | tostring(weaponAttack['Damage']['Electricity'] or 0), |
||
+ | if pinpoint then table.insert(cats, 'Pinpoint Weapons') end |
||
− | tostring(weaponAttack['Damage']['Heat'] or 0), |
||
+ | if regens then table.insert(cats, 'Battery Weapons') end |
||
− | tostring(weaponAttack['Damage']['Toxin'] or 0), |
||
+ | if silent then |
||
− | tostring(weaponAttack['Damage']['Blast'] or 0), |
||
+ | table.insert(cats, 'Silent Weapons') |
||
− | tostring(weaponAttack['Damage']['Corrosive'] or 0), |
||
+ | else |
||
− | tostring(weaponAttack['Damage']['Gas'] or 0), |
||
+ | table.insert(cats, 'Alarming Weapons') |
||
− | tostring(weaponAttack['Damage']['Magnetic'] or 0), |
||
+ | end |
||
− | tostring(weaponAttack['Damage']['Radiation'] or 0), |
||
+ | if single then table.insert(cats, 'Single Shot Weapons') end |
||
− | tostring(weaponAttack['Damage']['Viral'] or 0), |
||
+ | if spools then table.insert(cats, 'Spooling Weapons') end |
||
− | tostring(weaponAttack['Damage']['Void'] or 0), |
||
+ | if isHitScan then table.insert(cats, 'Hit-Scan Weapons') end |
||
− | tostring(weaponAttack['Damage']['MinProgenitorBonus'] or 0), |
||
+ | if isProjectile then table.insert(cats, 'Projectile Weapons') end |
||
− | tostring(baseDamage), |
||
+ | if isAoE then table.insert(cats, 'Weapons with Area of Effect') end |
||
− | tostring(totalDamage), |
||
+ | if isCodexSecret then table.insert(cats, 'Codex Secret') end |
||
− | tostring(weaponAttack['CritChance']), |
||
+ | if isTradable then |
||
− | tostring(weaponAttack['CritMultiplier']), |
||
+ | table.insert(cats, 'Tradeable Weapons') |
||
− | tostring(avgShot), |
||
+ | else |
||
− | tostring(avgBurst), |
||
+ | table.insert(cats, 'Untradeable Weapons') |
||
− | 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 |
||
+ | if isInConclave then table.insert(cats, 'Available In Conclave') end |
||
+ | return StatObject.cacheIn(weapAtk, 'Categories', cats) |
||
− | table.insert(csvResult, '</pre>') |
||
+ | end, function(s, cats) |
||
− | return table.concat(csvResult, '\n') |
||
+ | 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 |
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. |
--- Checks if a weapon is a variant or not. |
||
-- @function p._isVariant |
-- @function p._isVariant |
||
Line 544: | Line 903: | ||
function p._isVariant(weaponName) |
function p._isVariant(weaponName) |
||
for i, var in pairs(VARIANT_LIST) do |
for i, var in pairs(VARIANT_LIST) do |
||
− | if ( |
+ | if (var ~= "Dex" or weaponName ~= "Dex Pixia") then |
− | + | if string.find(weaponName, var) then |
|
− | return true, var, |
+ | return true, var, (string.gsub(weaponName, " ?"..var.." ?-?", "")) |
+ | end |
||
end |
end |
||
end |
end |
||
Line 555: | Line 915: | ||
-- @function p._buildName |
-- @function p._buildName |
||
-- @param {string} baseName Weapon's base name (e.g. "Braton") |
-- @param {string} baseName Weapon's base name (e.g. "Braton") |
||
− | -- @param |
+ | -- @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") |
-- @returns {string} Weapon's variant name (e.g. "Braton Vandal") |
||
function p._buildName(baseName, variant) |
function p._buildName(baseName, variant) |
||
− | if |
+ | if not variant or variant == 'Base' or variant == '' then |
return baseName |
return baseName |
||
− | elseif (baseName == 'Laser Rifle' and variant == 'Prime') then |
||
− | return variant..' '..baseName -- Laser Rifle has a primed version called 'Prime Laser Rifle' |
||
− | elseif (variant == 'Prime' or variant == 'Wraith' or variant == 'Vandal') then |
||
− | return baseName..' '..variant |
||
− | elseif (variant == 'MK1') then |
||
− | return 'MK1-'..baseName |
||
end |
end |
||
− | return |
+ | 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>. |
||
− | -- TODO: Function can be refactored |
||
− | --- Builds a list of weapons, with variants being next to base weapon name inside parentheses |
||
− | -- (e.g. Braton (MK1, Prime)). |
||
− | -- @function p._shortLinkList |
||
− | -- @param {table} Weapon Weapon table |
||
− | -- @param {boolean} tooltip If true, adds weapon tooltips, false otherwise; defaults to false |
||
− | -- @returns {string} Wikitext of resultant list |
||
− | function p._shortLinkList(Weapons, tooltip) |
||
− | -- First grabbing all the pieces and stashing them in a table |
||
− | local baseNames = {} |
||
− | for key, weap in Table.skpairs(Weapons) do |
||
− | local isVar, varType, baseName = p._isVariant(weap.Name) |
||
− | if (baseNames[baseName] == nil) then baseNames[baseName] = {} end |
||
− | table.insert(baseNames[baseName], varType) |
||
− | end |
||
− | |||
− | -- Then the fun part: Pulling the table together |
||
− | local result = {} |
||
− | for baseName, variants in Table.skpairs(baseNames) do |
||
− | -- So first, check if "Base" is in the list |
||
− | -- Because if it isn't, list all variants separately |
||
− | if (Table.contains(variants, "Base")) then |
||
− | table.sort(variants) |
||
− | -- First, get the basic version |
||
− | local thisRow = "" |
||
− | if (tooltip) then |
||
− | thisRow = "{{Weapon|"..baseName.."}}" |
||
− | else |
||
− | thisRow = "[["..baseName.."]]" |
||
− | end |
||
− | -- Then, if there are variants... |
||
− | if (Table.size(variants) > 1) then |
||
− | -- List them in parentheses one at a time |
||
− | thisRow = thisRow.." (" |
||
− | local count = 0 |
||
− | for i, varName in pairs(variants) do |
||
− | if (varName ~= "Base") then |
||
− | if (count > 0) then thisRow = thisRow..", " end |
||
− | if (tooltip) then |
||
− | thisRow = thisRow.."{{Weapon|"..p._buildName(baseName, varName).."|"..varName.."}}" |
||
− | else |
||
− | thisRow = thisRow.."[["..p._buildName(baseName, varName).."|"..varName.."]]" |
||
− | end |
||
− | count = count + 1 |
||
− | end |
||
− | end |
||
− | thisRow = thisRow..")" |
||
− | end |
||
− | table.insert(result, thisRow) |
||
− | else |
||
− | for i, varName in pairs(variants) do |
||
− | if (tooltip) then |
||
− | table.insert(result, "{{Weapon|"..p._buildName(baseName, varName).."}}") |
||
− | else |
||
− | table.insert(result, "[["..p._buildName(baseName, varName).."]]") |
||
− | end |
||
− | end |
||
− | end |
||
− | end |
||
− | return result |
||
− | end |
||
− | |||
− | --- Returns a specific weapon table entry from <code>/data</code>. |
||
-- @function p._getWeapon |
-- @function p._getWeapon |
||
-- @param {string} weaponName Weapon name |
-- @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 |
-- @returns {table} Weapon table |
||
− | function p._getWeapon(weaponName) |
+ | function p._getWeapon(weaponName, pvp) |
− | + | weaponName = mw.text.decode(weaponName) |
|
+ | return (pvp and ConclaveData or WeaponData)[weaponName] or |
||
− | if weapon ~= nil then |
||
+ | error('p._getWeapon(weaponName, pvp): "'..weaponName.. |
||
− | return weapon |
||
+ | '" does not exist in '..(pvp and '[[Module:Weapons/Conclave/data]]' or '[[Module:Weapons/data]]')) |
||
− | end |
||
− | |||
− | for key, Weapon in Table.skpairs(WeaponData["Weapons"]) do |
||
− | if (Weapon.Name == WeapName) then return Weapon end |
||
− | end |
||
− | error('p._getWeapon(weaponName): "'..weaponName..'" does not exist in M:Weapons/data') |
||
end |
end |
||
+ | --- Gets the raw value of a certain statistic of a weapon. |
||
− | --- Returns a specific weapon table entry from <code>/Conclave/data</code>. |
||
− | -- @function p. |
+ | -- @function p._getValue |
− | -- @param { |
+ | -- @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 |
||
− | function p._getConclaveWeapon(weaponName) |
||
+ | -- @returns {string, number} Value of statistic |
||
− | local weapon = ConclaveData["Weapons"][weaponName] |
||
+ | function p._getValue(weap, key, atk)--, formatted) |
||
− | if (weapon ~= nil and weapon.Name == weaponName) then |
||
+ | -- return (formatted and statFormat or statRead)(weap, atk, key) |
||
− | return weapon |
||
+ | return p._statRead(weap, atk, key) |
||
− | end |
||
− | |||
− | for key, Weapon in Table.skpairs(ConclaveData["Weapons"]) do |
||
− | if (Weapon.Name == WeapName or key == WeapName) then |
||
− | return Weapon |
||
− | end |
||
− | end |
||
− | error('p._getConclaveWeapon(weaponName): "'..weaponName..'" does not exist in M:Weapons/Conclave/data') |
||
end |
end |
||
− | --- |
+ | --- Gets the formatted value of a certain statistic of a weapon to be displayed |
+ | -- the wiki. |
||
− | -- @function getAttack |
||
+ | -- @function p._getFormattedValue |
||
− | -- @param {table, string} Weapon Weapon entry as seen in <code>/data</code> or the name of weapon |
||
+ | -- @param {table} Weapon Weapon table |
||
− | -- @param[opt] {string} attackName Name of attack key; if omitted, will default to "Attack1" |
||
− | -- @ |
+ | -- @param {string} keyName Name of key |
+ | -- @param[opt] {string} attackName Name of attack to search through; defaults to 'Attack1' |
||
− | local function getAttack(Weapon, attackName) |
||
+ | -- @returns {string} Value of statistic |
||
− | if (Weapon == nil or attackName == nil) then return end |
||
+ | function p._getFormattedValue(weap, key, atk) |
||
− | if (type(Weapon) == "string") then |
||
+ | -- return p._getValue(Weapon, keyName, attackName, true) |
||
− | Weapon = p._getWeapon(Weapon) |
||
+ | return p._statFormat(weap, atk, key) |
||
− | end |
||
− | if (not attackName) then |
||
− | return Weapon.Attack1 or Weapon.Damage and Weapon |
||
− | end |
||
− | return Weapon[attackName:find 'Attack' and attackName or 'Attack'..attackName] |
||
end |
end |
||
+ | --- Function that returns a simpler getter function, for multiple _stat*() calls on the same weapon/attack pair. |
||
− | --- Loops through all possible attacks that a weapon may have. |
||
− | -- @function p. |
+ | -- @function p._statReader |
− | -- @param {table} |
+ | -- @param {table} weap Weapon entry |
+ | -- @param {number|table} atk Attacks table index or Attack entry |
||
− | -- @returns {function} An iterator function that returns the key-value pair of next attack entry |
||
− | -- @ |
+ | -- @return {function} Getter function |
− | function p. |
+ | function p._statReader(weap, atk) |
+ | return function(...) return p._statRead(weap, atk, ...) end |
||
− | if (Weapon == nil) then |
||
− | return function() return nil end |
||
− | end |
||
− | local function nextAttack(t, k) |
||
− | if not k then return '1', t['Damage'] and t or t['Attack1'] end |
||
− | local v |
||
− | repeat |
||
− | k, v = next(t,k) |
||
− | until type(v) == 'table' and v['Damage'] |
||
− | return k, v |
||
− | end |
||
− | return nextAttack, Weapon |
||
end |
end |
||
+ | --- Function that returns a simpler getter function, for multiple _stat*() calls on the same weapon/attack pair. |
||
− | -- TODO: Not sure if we will use this in the future or not since augment mod data being |
||
+ | -- @function p._statFormatter |
||
− | -- stored in M:Weapons/data is not really appropriate |
||
+ | -- @param {table} weap Weapon entry |
||
− | local function getAugments(Weapon) |
||
+ | -- @param {number|table} atk Attacks table index or Attack entry |
||
− | local name = Weapon.Name ~= nil and Weapon.Name or Weapon |
||
+ | -- @return {function} Getter function |
||
− | local augments = {} |
||
+ | function p._statFormatter(weap, atk) |
||
− | for i, Augment in pairs(WeaponData["Augments"]) do |
||
+ | return function(...) return p._statFormat(weap, atk, ...) end |
||
− | for j, WeapName in pairs(Augment.Weapons) do |
||
− | if (WeapName == name) then |
||
− | table.insert(augments, Augment) |
||
− | end |
||
− | end |
||
− | end |
||
− | return augments |
||
end |
end |
||
+ | --- Returns a subset of <code>/data</code> or <code>/Conclave/data</code> based on a validation 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. |
||
− | -- @function getMeleeWeapons |
||
− | -- @param[opt] {string} weapClass Name of melee class to filter by; if nil then gets all melee weapons |
||
− | -- @param[opt] {boolean} PvP If true, only gets melee weapons available in Conclave, false otherwise; defaults to false |
||
− | -- @returns {table} An array of melee weapon table entries as seen in <code>/data</code> |
||
− | local function getMeleeWeapons(weapClass, PvP) |
||
− | local weaps = {} |
||
− | for i, weap in Table.skpairs(WeaponData["Weapons"]) do |
||
− | if ((weap.Ignore == nil or not weap.Ignore) and weap.Type ~= nil and weap.Type == "Melee") then |
||
− | local classMatch = (weapClass == nil or weap.Class == weapClass) |
||
− | local pvpMatch = (PvP == nil or (PvP and weap.Conclave ~= nil and weap.Conclave)) |
||
− | if (classMatch and pvpMatch) then |
||
− | table.insert(weaps, weap) |
||
− | end |
||
− | end |
||
− | end |
||
− | |||
− | return weaps |
||
− | end |
||
− | |||
− | -- TODO: Can probably be removed and refactored into getMeleeWeapons() |
||
− | --- Returns all melee weapons. If weapType is not nil, only grab for a specific type |
||
− | -- For example, if weapType is "Nikana", only pull Nikanas. |
||
− | -- @function getConclaveMeleeWeapons |
||
− | -- @param[opt] {string} weapClass Name of melee class to filter by; if nil then gets all melee weapons |
||
− | -- @param[opt] {boolean} PvP If true, only gets melee weapons available in Conclave, false otherwise; defaults to false |
||
− | -- @returns {table} An array of melee weapon table entries as seen in <code>/Conclave/data</code> |
||
− | local function getConclaveMeleeWeapons(weapClass, PvP) |
||
− | local weaps = {} |
||
− | local weapClasses = {} |
||
− | if (weapClass ~= nil) then |
||
− | weapClasses = String.split(weapClass, ",") |
||
− | end |
||
− | |||
− | for i, weap in Table.skpairs(ConclaveData["Weapons"]) do |
||
− | if ((weap.Ignore == nil or not weap.Ignore) and weap.Type ~= nil and weap.Type == "Melee") then |
||
− | local classMatch = (weapClass == nil or Table.contains(weapClasses, weap.Class)) |
||
− | local pvpMatch = (PvP == nil or (PvP and weap.Conclave ~= nil and weap.Conclave)) |
||
− | if (classMatch and pvpMatch) then |
||
− | table.insert(weaps, weap) |
||
− | end |
||
− | end |
||
− | end |
||
− | |||
− | return weaps |
||
− | end |
||
− | |||
− | --- Gets stance mods for a particular melee class. |
||
− | --- Returns a subset of <code>/data</code> based on a validation function. |
||
-- @function p._getWeapons |
-- @function p._getWeapons |
||
-- @param {function} validateFunction Function that filters out a weapon by taking in a Weapon table argument |
-- @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> |
-- @returns {table} Table of weapon table entries as seen in <code>/data</code> |
||
− | function p._getWeapons(validateFunction) |
+ | function p._getWeapons(validateFunction, source, opts) |
+ | opts=opts or {} |
||
− | local weaponList = {} |
||
+ | local ignoreIgnore, sortFunc, pvp = opts.ignoreIgnore, opts.sortFunc, opts.pvp |
||
− | for weaponName, weaponEntry in Table.skpairs(WeaponData["Weapons"]) do |
||
+ | validateFunction = validateFunction or function() return true end |
||
− | if (not Table.contains(WeaponData["IgnoreInCount"], weaponName) and |
||
+ | local data = pvp and ConclaveData or WeaponData |
||
− | (weaponEntry.Ignore == nil or not weaponEntry.Ignore) and validateFunction(weaponEntry)) then |
||
+ | if source then |
||
− | table.insert(weaponList, weaponEntry) |
||
+ | data = data[source] |
||
− | end |
||
end |
end |
||
− | return weaponList |
||
− | end |
||
− | -- TODO: Remove b/c unused in this module? |
||
− | --- Returns a subset of <code>/Conclave/data</code> based on a validation function. |
||
− | -- @function p._getConclaveWeapons |
||
− | -- @param {function} validateFunction Function that filters out a weapon by taking in a Weapon table argument |
||
− | -- @returns {table} Table of weapon table entries as seen in <code>/Conclave/data</code> |
||
− | function p._getConclaveWeapons(validateFunction) |
||
local weaps = {} |
local weaps = {} |
||
− | for |
+ | for _, weap in pairs(data) do |
− | if ( |
+ | if (ignoreIgnore or not weap['_IgnoreEntry']) and validateFunction(weap) then |
table.insert(weaps, weap) |
table.insert(weaps, weap) |
||
end |
end |
||
+ | end |
||
+ | if sortFunc ~= false then |
||
+ | table.sort(weaps, sortFunc or function(a, b) return a.Name < b.Name end) |
||
end |
end |
||
return weaps |
return weaps |
||
end |
end |
||
+ | --- Returns all melee weapons. If weapType is not nil, only grab for a specific type |
||
− | -- TODO: Move to M:Stances? |
||
+ | -- For example, if weapType is "Nikana", only pull Nikanas. |
||
− | --- Gets stance mods for a particular melee class. |
||
− | -- @function |
+ | -- @function p._getMeleeWeapons |
− | -- @param |
+ | -- @param[opt] {boolean} weapType |
− | -- @param |
+ | -- @param[opt] {boolean} pvp If true, only gets melee weapons available in Conclave, false otherwise; defaults to false |
− | -- @returns {table} |
+ | -- @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}) |
||
− | local stanceTable = {} |
||
− | |||
− | for stanceName, Stance in pairs(StanceData) do |
||
− | local typeMatch = (meleeClass == nil or meleeClass == Stance.WeaponType) |
||
− | local pvpMatch = (pvpOnly ~= nil and pvpOnly) or (Stance.ConclaveOnly == nil or not Stance.ConclaveOnly) |
||
− | if (typeMatch and pvpMatch) then |
||
− | stanceTable[stanceName] = Stance |
||
− | end |
||
− | end |
||
− | |||
− | return stanceTable |
||
end |
end |
||
+ | --- Main frame invokable function to access any raw/computed attribute/column/key of a weapon entry. |
||
− | --- Builds list of stances for a particular melee weapon as seen on [[Template:MeleeCategory]]. |
||
+ | -- See default table in M:Weapons to see all valid computed attributes. |
||
− | -- @function getWeaponStanceList |
||
+ | -- @function p.getValue |
||
− | -- @param {table} Weapon Wepaon table |
||
− | -- @ |
+ | -- @param {string} weap Weapon name in EN locale |
+ | -- @param {number} atk Attacks table index |
||
− | local function getWeaponStanceList(Weapon) |
||
+ | -- @param {string} k Key name |
||
− | if (Weapon == nil or Weapon.Type ~= "Melee") then return nil end |
||
+ | -- @return Raw or computed value associated with k key |
||
− | local stanceTable = {} |
||
+ | function p.getValue(frame) |
||
− | if Weapon.Class == "Exalted Weapon" then |
||
+ | -- table.unpack doesn't work on the frame object which is why this is anonymous function is needed |
||
− | stanceTable = getStances(Weapon.Name, Weapon.Conclave) |
||
+ | local weap, key, atk = (function(t) return t[1], t[2], t[3] end)(frame.args) |
||
− | else |
||
+ | weap = p._getWeapon(weap) |
||
− | stanceTable = getStances(Weapon.Class, Weapon.Conclave) |
||
+ | return p._getValue(weap, key, atk) |
||
− | end |
||
− | |||
− | local result = "" |
||
− | |||
− | for stanceName, Stance in pairs(stanceTable) do |
||
− | if (string.len(result) > 0) then |
||
− | result = result.."<br/>" |
||
− | end |
||
− | |||
− | local polarity = "" |
||
− | local link = "" |
||
− | if Weapon.Class == "Exalted Weapon" then |
||
− | link = "[["..ModData["Mods"][stanceName].Link.."|"..stanceName.."]]" |
||
− | else |
||
− | polarity = " ("..Polarity._polarity(ModData["Mods"][stanceName].Polarity)..")" |
||
− | link = "[["..stanceName.."]]" |
||
− | end |
||
− | |||
− | -- Adding tooltip |
||
− | result = result..Tooltip.getIcon(stanceName, "Mods") |
||
− | |||
− | -- If this is a PvP Stance, add the disclaimer |
||
− | if (Stance.ConclaveOnly ~= nil and Stance.ConclaveOnly) then |
||
− | result = result.." (PvP Only)" |
||
− | end |
||
− | end |
||
− | |||
− | return result |
||
end |
end |
||
+ | --- Main frame invokable function to access any formatted attribute/column/key of a weapon entry. |
||
− | -- TODO: Pull stance data from M:Stance/data? |
||
+ | -- See default table in M:Weapons to see all valid computed attributes. |
||
− | --- Builds list of stances for a particular melee weapon as seen on [[Template:MeleeCategory]]. |
||
− | -- @function p. |
+ | -- @function p.getFormattedValue |
− | -- @param { |
+ | -- @param {string} weap Weapon name in EN locale |
+ | -- @param {number} atk Attacks table index |
||
− | -- @returns {string} Resultant wikitext of comparison list |
||
+ | -- @param {string} k Key name |
||
− | function p.getWeaponStanceList(frame) |
||
+ | -- @return Formatted value associated with k key |
||
− | local weaponName = frame.args ~= nil and frame.args[1] or frame |
||
+ | function p.getFormattedValue(frame) |
||
− | local Weapon = p._getWeapon(weaponName) |
||
+ | local weap, key, atk = (function(t) return t[1], t[2], t[3] end)(frame.args) |
||
− | return getWeaponStanceList(Weapon) |
||
+ | weap = p._getWeapon(weap) |
||
+ | return p._getFormattedValue(weap, key, atk) |
||
end |
end |
||
+ | --- Builds a melee weapon gallery as seen on [[Template:MeleeCategory]]. |
||
− | --- Gets the value of a certain statistic of a weapon. |
||
− | -- @function getValue |
||
− | -- @param {table} Weapon Weapon table |
||
− | -- @param {string} keyName Name of key |
||
− | -- @param[opt] {string} attackName Name of attack to search through |
||
− | -- @returns {string, number} Value of statistic |
||
− | local function getValue(Weapon, keyName, attackName) |
||
− | -- Trying to use pcall to get more specific errors |
||
− | -- for _, map in pairs({ SHARED_KEY_MAP, GUN_KEY_MAP, MELEE_KEY_MAP, ATTACK_KEY_MAP }) do |
||
− | -- if (map[keyName] ~= nil) then |
||
− | -- local status, result = pcall(map[keyName], Weapon) |
||
− | -- end |
||
− | -- end |
||
− | -- error('getValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" in '..mw.dumpObject(Weapon)) |
||
− | |||
− | return SHARED_KEY_MAP[keyName] ~= nil and SHARED_KEY_MAP[keyName](Weapon) or |
||
− | GUN_KEY_MAP[keyName] ~= nil and GUN_KEY_MAP[keyName](Weapon) or |
||
− | MELEE_KEY_MAP[keyName] ~= nil and MELEE_KEY_MAP[keyName](Weapon) or |
||
− | ATTACK_KEY_MAP[keyName] ~= nil and ATTACK_KEY_MAP[keyName](Weapon[attackName or Weapon['TooltipAttackDisplay'] or 'Attack1']) or |
||
− | error('getValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" in '..mw.dumpObject(Weapon)) |
||
− | end |
||
− | |||
− | -- TODO: Move this function to M:Stance? |
||
− | --- Builds list of weapons that can equip a particlar stance mod as seen on [[Template:StanceWeapons]]. |
||
− | -- @function p.getStanceWeaponList |
||
− | -- @param {table} frame Frame object w/ first argument being string stanceName |
||
− | -- @returns {string} Resultant wikitext of comparison list |
||
− | function p.getStanceWeaponList(frame) |
||
− | local stanceName = frame.args ~= nil and frame.args[1] or frame |
||
− | local Stance = StanceData[stanceName] |
||
− | assert(Stance ~= nil, "p.getStanceWeaponList(frame): "..stanceName.." not found") |
||
− | |||
− | local weaps = getMeleeWeapons(Stance.WeaponType, Stance.ConclaveOnly) |
||
− | local result = "" |
||
− | |||
− | for i, weap in Table.skpairs(weaps) do |
||
− | if (string.len(result) > 0) then |
||
− | result = result.."\n" |
||
− | end |
||
− | |||
− | if (Stance.ConclaveOnly) then |
||
− | result = result.."*[[Conclave:"..weap.Name.."|"..weap.Name.."]]" |
||
− | else |
||
− | result = result.."*[["..weap.Name.."]]" |
||
− | end |
||
− | |||
− | if (weap.StancePolarity == ModData["Mods"][stanceName].Polarity) then |
||
− | result = result.." ✓" |
||
− | end |
||
− | end |
||
− | |||
− | return result |
||
− | end |
||
− | |||
− | --- Builds a weapon gallery. |
||
− | -- @function getWeaponGallery |
||
− | -- @param {table} Weapons Array of weapon table entries to be displayed |
||
− | -- @returns {string} Resultant wikitext of gallery |
||
− | local function getWeaponGallery(Weapons) |
||
− | local result = { '{| style="margin:auto;text-align:center;"' } |
||
− | local nameRow = "" |
||
− | for i, Weapon in pairs(Weapons) do |
||
− | local theImage = Weapon.Image ~= nil and Weapon.Image or "Panel.png" |
||
− | if ((i - 1) % 5 == 0) then |
||
− | table.insert(result, nameRow.."\n|-") |
||
− | nameRow = "\n|-" |
||
− | end |
||
− | table.insert(result, '| style="width:165px" |[[File:'..theImage..'|150px|link='..Weapon.Name..']]') |
||
− | nameRow = nameRow..'\n| style="vertical-align: text-top;" |[['..Weapon.Name..']]' |
||
− | end |
||
− | table.insert(result, nameRow) |
||
− | table.insert(result, '|}') |
||
− | return table.concat(result, '\n') |
||
− | end |
||
− | |||
− | --- Builds a melee weapon gallery as seen on [[Template:MeleeCategory]]. |
||
-- @function p.getMeleeWeaponGallery |
-- @function p.getMeleeWeaponGallery |
||
-- @param {table} frame Frame object w/ first argumenting being string meleeClass |
-- @param {table} frame Frame object w/ first argumenting being string meleeClass |
||
-- @returns {string} Resultant wikitext of gallery |
-- @returns {string} Resultant wikitext of gallery |
||
function p.getMeleeWeaponGallery(frame) |
function p.getMeleeWeaponGallery(frame) |
||
− | local meleeClass = |
+ | local meleeClass = frame.args[1] or '' |
+ | local result = { "=="..meleeClass.." Weapons==", '<gallery widths="200" position="center" spacing="small">' } |
||
− | local WeapArray = getMeleeWeapons(meleeClass) |
||
− | + | for i, weap in ipairs(p._getMeleeWeapons(meleeClass)) do |
|
+ | table.insert(result, p._statRead(weap, nil, 'Image')..'|'..p._statFormat(weap, nil, 'Name')) |
||
− | result = result..getWeaponGallery(WeapArray) |
||
+ | end |
||
− | return result |
||
+ | table.insert(result, '</gallery>') |
||
+ | return frame:preprocess(table.concat(result, '\n')) -- annoying that it needs to be preprocessed |
||
end |
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 |
+ | -- @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 {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(frame) |
||
+ | function p._getWeaponCount(slot) |
||
− | local weaponType = frame.args ~= nil and frame.args[1] or frame |
||
+ | slot = slot and slot:lower() |
||
− | local getFullList = frame.args ~= nil and frame.args[2] |
||
− | local |
+ | local data = slot and WeaponData[slot] or WeaponData |
− | local |
+ | local fullList = {} |
+ | |||
− | local fullList = "" |
||
− | for |
+ | for name, weapon in pairs(data) do |
+ | if not weapon._IgnoreInMasteryCount then |
||
− | if (not Table.contains(WeaponData["IgnoreInCount"], i) and getAll ~= "true") or getAll == "true" then |
||
+ | -- TODO: There should be a better way to determine/differentiate if a weapon is a kitgun b/c kitguns and zaws |
||
− | if (weaponType == nil or weaponType == "") then |
||
+ | -- are stored in the same M:Weapons/data/modular data store; add a new "Kitgun" or "Zaw" Trait and target that? |
||
− | count = count + 1 |
||
+ | if (slot == 'kitgun' and weapon.Slot == 'Secondary') |
||
− | if (getFullList ~= nil) then fullList = fullList.."\n# "..val.Name end |
||
+ | or (slot == 'zaw' and weapon.Slot == 'Melee') |
||
− | elseif (weaponType == "Warframe") then |
||
+ | or (slot == 'robotic' and weapon.Slot ~= 'Hound') |
||
− | if ((val.Type == "Primary" or val.Type == "Secondary" or val.Type == "Melee") and val.Class ~= "Exalted Weapon") then |
||
+ | or (weapon.Slot:lower() == slot) |
||
− | count = count + 1 |
||
− | + | or slot == nil then |
|
+ | fullList[#fullList + 1] = name |
||
− | 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 |
||
− | if (getFullList ~= nil) then return fullList end |
||
− | return count |
||
− | end |
||
+ | return #fullList, fullList |
||
− | --- Gets the weapon class of secondary weapons. |
||
− | -- @function getSecondaryCategory |
||
− | -- @param {table} weapon Weapon table |
||
− | -- @returns {string} Category name |
||
− | local function getSecondaryCategory(weapon) |
||
− | local class = getValue(weapon, "Class") |
||
− | if (class == "Thrown") then |
||
− | return "Thrown" |
||
− | elseif (class == "Dual Shotguns" or class == "Shotgun Sidearm") then |
||
− | return "Shotgun" |
||
− | else |
||
− | local trigger = getValue(weapon, "Trigger") |
||
− | if (trigger == "Semi-Auto" or trigger == "Burst") then |
||
− | return "Semi-Auto" |
||
− | elseif (trigger == "Auto" or trigger == "Auto-Spool") then |
||
− | return "Auto" |
||
− | end |
||
− | end |
||
− | return "Other" |
||
end |
end |
||
− | --- |
+ | --- Gets the total count of weapons as used on [[Mastery Rank#Total Mastery]]. |
− | -- |
+ | -- @function p.getWeaponCount |
− | -- @param {table} |
+ | -- @param {table} frame Frame object w/ the first argument being the weapon slot |
+ | -- @return {number} Total number of weapons that can reward Mastery XP |
||
− | -- @returns {string} Category name |
||
− | + | function p.getWeaponCount(frame) |
|
+ | return (p._getWeaponCount(frame.args and frame.args[1] or nil)) |
||
− | local class = getValue(weapon, "Class") |
||
− | if (class == "Shotgun") then |
||
− | return "Shotgun" |
||
− | elseif (class == "Bow") then |
||
− | return "Bow" |
||
− | elseif (class == "Sniper Rifle") then |
||
− | return "Sniper" |
||
− | elseif (class == "Rifle") then |
||
− | local trigger = getValue(weapon, "Trigger") |
||
− | if (trigger == "Semi-Auto" or trigger == "Burst") then |
||
− | return "Semi-Auto" |
||
− | elseif (trigger == "Auto" or trigger == "Auto-Spool") then |
||
− | return "Auto" |
||
− | end |
||
− | end |
||
− | return "Other" |
||
end |
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 resultant wikitable |
-- @returns {string} Wikitext of resultant wikitable |
||
function p.getPolarityTable(frame) |
function p.getPolarityTable(frame) |
||
+ | local colNames = { 'Primary', 'Secondary', 'Melee', 'Archgun', 'Archmelee' } |
||
− | local tableResult = { [[ |
||
+ | local cols = {} -- Will look like: {['Primary']={},['Secondary']={},['Melee']={},['Archgun']={},['Archmelee']={},} |
||
− | {| style="width: 100%; border-collapse: collapse;" cellpadding="2" border="1" |
||
+ | local colOrder = {} --{cols['Primary'],cols['Secondary'],cols['Melee'],cols['Archgun'],cols['Archmelee'],} |
||
− | |+ '''Weapons with Innate Polarities (ignoring Stance and Exilus slots)''' |
||
+ | local colCounts = {} |
||
− | ! 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 |
||
+ | for i, v in ipairs(colNames) do |
||
− | --- Builds comparison string between two values. |
||
+ | cols[v] = {} |
||
− | -- @function p.buildCompareString |
||
+ | colOrder[i] = cols[v] |
||
− | -- @param {string, number} firstVal Value used for comparison |
||
+ | colCounts[v] = 0 |
||
− | -- @param {string, number} secondVal Value used to compare the first value against |
||
− | -- @param {string} valName Name of statistic that values represent (e.g. "Critical Damage") |
||
− | -- @param[opt] {number} digits The decimal to round the values by (e.g. if you want to round to two decimal places put in 0.01) |
||
− | -- @param[opt] {string} unit The values' unit (e.g. "m" or "seconds") |
||
− | -- @param[opt] {table} compareAdjs Two element table that contains the greater than and less than comparative adjectives (e.g. { "Higher", "Lower" } ) |
||
− | -- @param[opt] {string} start What to start the comparison string by for if you want to increase the bullet level (e.g. "\n***") |
||
− | -- @returns {string} Resultant wikitext of comparison string |
||
− | local function buildCompareString(firstVal, secondVal, valName, digits, unit, compareAdjs, start) |
||
− | if (firstVal == nil or secondVal == nil) then |
||
− | return "" |
||
end |
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 |
||
+ | for _, weapon in pairs(WeaponData) do |
||
− | --- Builds damage comparison string between two attacks. |
||
+ | local pols = Table.size(weapon["Polarities"] or {}) |
||
− | -- @function p.buildComparison |
||
+ | local slot = weapon['Slot'] |
||
− | -- @param {table} Attack1 Attack used for comparison |
||
+ | if pols > 0 and cols[slot] then |
||
− | -- @param {table} Attack2 Attack used to compare the first attack against |
||
+ | table.insert(cols[slot], { |
||
− | -- @returns {string} Resultant wikitext of comparison string |
||
+ | '|'..p._getFormattedValue(weapon, 'NameLink'):gsub(' ?%(.*%)', '')..'||'..p._getFormattedValue(weapon, "Polarities"), |
||
− | local function buildDamageTypeComparisonString(Attack1, Attack2) |
||
+ | pols |
||
− | local result = "" |
||
+ | }) |
||
− | --ipairs iterates in the order given in Elements, so IPS is always first |
||
+ | colCounts[slot] = colCounts[slot] + 1 |
||
− | 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 |
||
end |
end |
||
− | return result |
||
− | end |
||
+ | for i, v in ipairs(colNames) do |
||
− | -- TODO: The above TODO can be done in conjunction with changing all the "Attack1" and "Attack2" keys |
||
+ | colCounts[i] = colCounts[v] |
||
− | -- to a singular "Attack" key with indexed table elements |
||
+ | table.sort(cols[v], function(a, b)return a[2] > b[2] end) |
||
− | --- Builds comparison list between two gun weapons. |
||
− | -- @function p.buildComparison |
||
− | -- @param {table} Weapon1 Weapon used for comparison |
||
− | -- @param {table} Weapon2 Weapon used to compare the first weapon against |
||
− | -- @param {boolean} conclave If true, makes comparison list based on PvP stats, otherwise uses PvE stats; default false |
||
− | -- @returns {string} Resultant wikitext of comparison list |
||
− | local function buildGunComparisonString(Weapon1, Weapon2, Conclave) |
||
− | local result = {} |
||
− | -- Adding this assignment to support method chaining w/ colon syntax |
||
− | result.insert = function(self, elem) table.insert(self, elem) return self end |
||
− | |||
− | local Att1 = getAttack(Weapon1, Weapon1['TooltipAttackDisplay'] or 'Attack1') |
||
− | local Att2 = getAttack(Weapon2, Weapon2['TooltipAttackDisplay'] or 'Attack1') |
||
− | if (Conclave) then |
||
− | result:insert("* [["..Weapon1.Name.."]], compared to [[Conclave:"..Weapon2.Name.."|"..Weapon2.Name.."]]:") |
||
− | else |
||
− | result:insert("* [["..Weapon1.Name.."]] (attack: "..(Att1.AttackName or "Normal").."), compared to [["..Weapon2.Name.."]] (attack: "..(Att2.AttackName or "Normal").."):") |
||
end |
end |
||
− | |||
− | local dmgString = "" |
||
− | dmgString = dmgString..buildCompareString(getValue(Weapon1, "BaseDamage"), getValue(Weapon2, "BaseDamage"), "base damage", 0.01) |
||
− | dmgString = dmgString..buildDamageTypeComparisonString(Att1, Att2) |
||
− | if (string.len(dmgString) > 0 and getValue(Weapon1, "BaseDamage") == getValue(Weapon2, "BaseDamage")) then |
||
− | dmgString = "\n**Equal base damage, but different composition:"..dmgString |
||
− | end |
||
− | dmgString = dmgString..buildCompareString(getValue(Weapon1, "TotalDamage"), getValue(Weapon2, "TotalDamage"), "total damage", 0.01) |
||
− | -- TODO: Find a better way of finding out if an attack is a charge attack; maybe add Trigger = "Charge" to all of those attacks? |
||
− | -- result = result..buildCompareString(getValue(Weapon1, "ChargeTime", "Attack3"), getValue(Weapon2, "ChargeTime", "Attack3"), "charge time", 0.01, " s", {"Slower", "Faster"}) |
||
− | result:insert(dmgString) |
||
− | |||
− | if (not Conclave) then |
||
− | result:insert( |
||
− | buildCompareString((Att1.CritChance * 100), (Att2.CritChance * 100), "base [[critical chance]]", 0.01, "%") |
||
− | ):insert( |
||
− | buildCompareString(Att1.CritMultiplier, Att2.CritMultiplier, "base [[critical multiplier]]", 0.01, "x") |
||
− | ):insert( |
||
− | buildCompareString((Att1.StatusChance * 100), (Att2.StatusChance * 100), "base [[status chance]]", 0.01, "%") |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "AvgShotDmg"), getValue(Weapon2, "AvgShotDmg"), "[[Damage#Final_Calculations|average damage per shot]] (max Progenitor bonus if applicable)", 0.01) |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "BurstDps"), getValue(Weapon2, "BurstDps"), "[[Damage#Final_Calculations|burst DPS]] (max Progenitor bonus if applicable)", 0.01) |
||
− | ):insert(buildCompareString(getValue(Weapon1, "SustainedDps"), getValue(Weapon2, "SustainedDps"), "[[Damage#Final_Calculations|sustained DPS]] (max Progenitor bonus if applicable)", 0.01) |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "FalloffStart"), getValue(Weapon2, "FalloffStart"), "starting [[Damage Falloff|damage falloff]] distance", 0.1, "m", {"Farther", "Closer"}) |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "FalloffEnd"), getValue(Weapon2, "FalloffEnd"), "ending damage falloff distance", 0.1, "m", {"Farther", "Closer"}) |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "FalloffReduction") * 100, getValue(Weapon2, "FalloffReduction") * 100, "max damage reduction at ending falloff distance", 0.01, "%", {"Greater", "Lesser"}) |
||
− | ) |
||
− | end |
||
− | |||
− | result:insert( |
||
− | buildCompareString(Att1.FireRate, Att2.FireRate, "[[fire rate]]", 0.01, " round(s)/sec") |
||
− | ):insert( |
||
− | buildCompareString(Att1.Multishot or 1, Att2.Multishot or 1, "[[multishot]]", 1, " projectile(s)") |
||
− | ):insert( |
||
− | buildCompareString(Weapon1.Magazine, Weapon2.Magazine, "magazine", 1, " round(s)", {"Larger", "Smaller"}) |
||
− | ):insert( |
||
− | buildCompareString(Weapon1.MaxAmmo, Weapon2.MaxAmmo, "max ammo capacity", 1, " round(s)", {"Larger", "Smaller"}) |
||
− | ):insert( |
||
− | buildCompareString(Weapon1.Reload, Weapon2.Reload, "[[Reload|reload time]]", 0.01, " s", {"Slower", "Faster"}) |
||
− | ):insert( |
||
− | buildCompareString(Weapon1.Spool, Weapon2.Spool, "spool-up", 1, " round(s)", {"Slower", "Faster"}) |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "Accuracy"), getValue(Weapon2, "Accuracy"), "[[Accuracy|accurate]]", 0.01, nil, {"More", "Less"}) |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "Polarities"), getValue(Weapon2, "Polarities"), "[[Polarity|polarities]]", nil, nil, {"Different", "Different"}) |
||
− | ):insert( |
||
− | buildCompareString(Weapon1.Mastery, Weapon2.Mastery, "[[Mastery Rank]] required", 1) |
||
− | ):insert( |
||
− | buildCompareString(Weapon1.Disposition, Weapon2.Disposition, "[[disposition]]", 0.01) |
||
− | ) |
||
− | |||
− | --Handling Syndicate radial effects |
||
− | if (Weapon1.SyndicateEffect ~= nil and Weapon2.SyndicateEffect == nil) then |
||
− | result:insert("\n** Innate [["..Weapon1.SyndicateEffect.."]] effect") |
||
− | elseif (Weapon2.SyndicateEffect ~= nil and Weapon1.SyndicateEffect == nil) then |
||
− | result:insert("\n** Lack of an innate [["..Weapon2.SyndicateEffect.."]] effect") |
||
− | elseif (Weapon1.SyndicateEffect ~= nil and Weapon2.SyndicateEffect ~= nil and |
||
− | Weapon1.SyndicateEffect ~= Weapon2.SyndicateEffect2) then |
||
− | result:insert("\n** Different innate [[Syndicate Radial Effects|Syndicate Effect]]: [[".. |
||
− | Weapon1.SyndicateEffect.."]] vs. [["..Weapon2.SyndicateEffect.."]]") |
||
− | end |
||
− | return table.concat(result) |
||
− | end |
||
+ | local result = {[=[ |
||
− | --- Builds comparison list between two melee or arch-melee weapons. |
||
+ | {| style="width: 100%; border-collapse: collapse;" cellpadding="2" border="1" |
||
− | -- @function p.buildComparison |
||
+ | |+ '''Weapons with Innate Polarities (ignoring Stance and Exilus slots)''' |
||
− | -- @param {table} Weapon1 Weapon used for comparison |
||
+ | ! colspan="2" |Primaries |
||
− | -- @param {table} Weapon2 Weapon used to compare the first weapon against |
||
+ | ! colspan="2" |Secondaries |
||
− | -- @param {boolean} conclave If true, makes comparison list based on PvP stats, otherwise uses PvE stats; default false |
||
+ | ! colspan="2" |Melees |
||
− | -- @returns {string} Resultant wikitext of comparison list |
||
+ | ! colspan="2" |Archguns |
||
− | local function buildMeleeComparisonString(Weapon1, Weapon2, Conclave) |
||
+ | ! colspan="2" |Archmelees]=]} |
||
− | local result = {} |
||
+ | for i = 1, math.max(table.unpack(colCounts)) do --row |
||
− | -- Adding this assignment to support method chaining w/ colon syntax |
||
+ | table.insert(result, '|-') |
||
− | result.insert = function(self, elem) table.insert(self, elem) return self end |
||
+ | for _, col in ipairs(colOrder) do --cell |
||
− | |||
+ | table.insert(result,(col[i] or {'| ||'})[1]) |
||
− | local Att1 = getAttack(Weapon1, Weapon1['TooltipAttackDisplay'] or 'Attack1') |
||
− | local Att2 = getAttack(Weapon2, Weapon2['TooltipAttackDisplay'] or 'Attack1') |
||
− | |||
− | if (Conclave) then |
||
− | result:insert("* "..Weapon1.Name..", compared to [[Conclave:"..Weapon2.Name.."|"..Weapon2.Name.."]]:") |
||
− | else |
||
− | result:insert("* "..Weapon1.Name.." (attack: "..(Att1.AttackName or "Normal").."), compared to [["..Weapon2.Name.."]] (attack: "..(Att2.AttackName or "Normal").."):") |
||
− | end |
||
− | |||
− | local dmgString = "" |
||
− | dmgString = dmgString..buildCompareString(getValue(Weapon1, "BaseDamage"), getValue(Weapon2, "BaseDamage"), "base damage", 0.01) |
||
− | dmgString = dmgString..buildDamageTypeComparisonString(Att1, Att2) |
||
− | if (string.len(dmgString) > 0 and getValue(Weapon1, "BaseDamage") == getValue(Weapon2, "BaseDamage")) then |
||
− | dmgString = "\n**Equal base damage, but different composition:"..dmgString |
||
− | end |
||
− | result:insert(dmgString) |
||
− | |||
− | if (not Conclave) then |
||
− | result:insert( |
||
− | buildCompareString((Att1.CritChance * 100), (Att2.CritChance * 100), "[[critical chance]]", 0.01, "%") |
||
− | ):insert( |
||
− | buildCompareString(Att1.CritMultiplier, Att2.CritMultiplier, "[[critical multiplier]]", 0.01, "x") |
||
− | ):insert( |
||
− | buildCompareString((Att1.StatusChance * 100), (Att2.StatusChance * 100), "[[status chance]]", 0.01, "%") |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "MeleeRange"), getValue(Weapon2, "MeleeRange"), "Range", 0.01, " m") |
||
− | ):insert( |
||
− | buildCompareString(Weapon1.Disposition, Weapon2.Disposition, "[[disposition]]", 0.01) |
||
− | ) |
||
− | end |
||
− | |||
− | result:insert( |
||
− | buildCompareString(Att1.FireRate, Att2.FireRate, "[[attack speed]]", 0.01) |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "Polarities"), getValue(Weapon2, "Polarities"), "[[Polarity|polarities]]", nil, nil, {"Different", "Different"}) |
||
− | ):insert( |
||
− | buildCompareString(Weapon1.Mastery, Weapon2.Mastery, "[[Mastery Rank]] required", 1) |
||
− | ) |
||
− | |||
− | -- Quick fix to passing in an empty string for polarity when comparing with an arch-melee weapon |
||
− | if (Weapon1.Type ~= "Arch-Melee" and Weapon2.Type ~= "Arch-Melee") then |
||
− | result:insert( |
||
− | buildCompareString(getValue(Weapon1, "ComboDur"), getValue(Weapon2, "ComboDur"), "[[Melee Combo|Combo Duration]]", 1, " s") |
||
− | ):insert( |
||
− | buildCompareString(getValue(Weapon1, "BlockAngle"), getValue(Weapon2, "BlockAngle"), "Block Angle", 1, "°") |
||
− | ):insert( |
||
− | buildCompareString(Polarity._polarity(Weapon1.StancePolarity), Polarity._polarity(Weapon2.StancePolarity), "[[Stance]] Polarity", nil, nil, {"Different", "Different"}) |
||
− | ) |
||
− | end |
||
− | return table.concat(result) |
||
− | end |
||
− | |||
− | --- Builds comparison list between two weapons in PvE. |
||
− | -- @function p.buildComparison |
||
− | -- @param {table} frame Frame object |
||
− | -- @returns {string} Resultant wikitext of comparison list |
||
− | function p.buildComparison(frame) |
||
− | local weaponName1 = frame.args[1] |
||
− | local weaponName2 = frame.args[2] |
||
− | |||
− | assert(weaponName1 ~= '' and weaponName2 ~= '', 'p.buildComparison(frame): Must compare two weapons') |
||
− | |||
− | local Weapon1 = p._getWeapon(weaponName1) |
||
− | local Weapon2 = p._getWeapon(weaponName2) |
||
− | |||
− | local comparisonString = '' |
||
− | if (Weapon1.Type == 'Melee' or Weapon1.Type == 'Arch-Melee') then |
||
− | comparisonString = buildMeleeComparisonString(Weapon1, Weapon2) |
||
− | else |
||
− | comparisonString = buildGunComparisonString(Weapon1, Weapon2) |
||
− | end |
||
− | return comparisonString..'[[Category:Automatic Comparison]]' |
||
− | end |
||
− | |||
− | --- Builds comparison list between two weapons in PvP ([[Conclave]]). |
||
− | -- @function p.buildComparison |
||
− | -- @param {table} frame Frame object |
||
− | -- @returns {string} Resultant wikitext of comparison list |
||
− | function p.buildConclaveComparison(frame) |
||
− | local weaponName1 = frame.args[1] |
||
− | local weaponName2 = frame.args[2] |
||
− | |||
− | assert(weaponName1 ~= '' and weaponName2 ~= '', 'p.buildConclaveComparison(frame): Must compare two weapons') |
||
− | |||
− | local Weapon1 = p._getConclaveWeapon(weaponName1) |
||
− | local Weapon2 = p._getConclaveWeapon(weaponName2) |
||
− | |||
− | local comparisonString = '' |
||
− | if (Weapon1.Type == 'Melee' or Weapon1.Type == 'Arch-Melee') then |
||
− | comparisonString = buildMeleeComparisonString(Weapon1, Weapon2, true) |
||
− | else |
||
− | comparisonString = buildGunComparisonString(Weapon1, Weapon2, true) |
||
− | end |
||
− | return comparisonString..'[[Category:Automatic Comparison]]' |
||
− | end |
||
− | |||
− | --- Adds weapon categories. |
||
− | -- @function p.buildAutoboxCategories |
||
− | -- @param {table} frame Frame object |
||
− | -- @returns {string} Wikitext of category links |
||
− | function p.buildAutoboxCategories(frame) |
||
− | local WeapName = frame.args ~= nil and frame.args[1] or frame |
||
− | local Weapon = p._getWeapon(WeapName) |
||
− | local result = { "[[Category:Automatic Weapon Box]][[Category:Weapons]]" } |
||
− | if (Weapon == nil or (Weapon.IgnoreCategories ~= nil and Weapon.IgnoreCategories)) then |
||
− | return "" |
||
− | end |
||
− | table.insert(result, CATEGORY_MAP[Weapon['Class']]..CATEGORY_MAP[Weapon['Trigger']]..CATEGORY_MAP[Weapon['Type']]) |
||
− | |||
− | -- TODO: Archive this category b/c not sure how important it is or how often |
||
− | -- it is used; augments are moved out of M:Weapons/data and should use M:Mods/data |
||
− | local augments = getAugments(Weapon) |
||
− | if (Table.size(augments) > 0) then |
||
− | table.insert(result, "[[Category:Augmented Weapons]]") |
||
− | end |
||
− | |||
− | -- Adding appropriate categories to page based on weapon's categorical traits |
||
− | for _, trait in pairs(Weapon['Traits'] or {}) do |
||
− | table.insert(result, CATEGORY_MAP[trait]) |
||
− | end |
||
− | |||
− | local bestPercent, bestElement = getDamageBias(Weapon[Weapon['TooltipAttackDisplay'] or 'Attack1']) |
||
− | if (bestElement == "Impact" or bestElement == "Puncture" or bestElement == "Slash") then |
||
− | if (bestPercent > .38) then |
||
− | table.insert(result, "[[Category:"..bestElement.." Damage Weapons]]") |
||
− | else |
||
− | table.insert(result, "[[Category:Balanced Physical Damage Weapons]]") |
||
end |
end |
||
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 |
+ | 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 !! Slot !! Class !! Attack Name !! data-sort-type="number" | %s !! data-sort-type="number" | Majority |
||
|- |
|- |
||
+ | ]]):format(mostly and 'mostly ' or '', damageType, Tooltip.full(damageType, 'DamageTypes')) |
||
− | ! Name !! Type !! Class !! %s !! %s%% |
||
+ | ..table.concat(content, '\n|-\n')..'\n|}' |
||
− | ]], damageType, procString, procShort) |
||
+ | 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 |
||
− | local tRows = {} |
||
+ | -- (e.g. {{Weapon|Braton}} ({{Weapon|MK1-Braton|MK1}}, {{Weapon|Braton Prime|Prime}})). |
||
− | for i, Weapon in pairs(WeapArray) do |
||
+ | -- @function p._shortLinkList |
||
− | local thisRow = [[ |
||
+ | -- @param {table} Weapon Weapon table |
||
− | |- |
||
+ | -- @param {boolean} tooltip If true, adds weapon tooltips, false otherwise; defaults to false |
||
− | | {{Weapon|%s}} || %s || %s || %s || data-sort-value="%s" | %s]] |
||
+ | -- @returns {string} Wikitext of resultant list |
||
− | local weaponLink = weapLink(Weapon) |
||
+ | function p._shortLinkList(Weapons, tooltip) |
||
− | assert(Weapon.Type ~= nil, 'p.buildDamageTypeTable(frame): "'..weaponLink..'" has a nil value for Type"') |
||
+ | return StatObject.shortLinkList(Weapons, variantOf, tooltip and 'Weapons') |
||
− | 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 |
||
− | --- |
+ | --- Builds a list of weapons' mastery requirements as seen on [[Template:EquipmentUnlock]], |
+ | -- [[Template:EquipmentUnlock/Primary]], [[Template:EquipmentUnlock/Secondary]], |
||
− | -- @function p.getMasteryShortList |
||
+ | -- [[Template:EquipmentUnlock/Melee]], etc. |
||
− | -- @param {table} frame Frame object w/ first argument being a string weaponType |
||
+ | -- @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 |
+ | 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 as seen on [[PvP#Limitations]]. |
− | -- |
+ | -- @function p.getConclaveList |
− | -- @param {table} frame Frame object w/ first argument being a string |
+ | -- @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 |
+ | local weaponSlot = frame.args[1] or 'All' |
− | local weapArray = p._getWeapons(function( |
+ | 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 |
+ | 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. |
+ | 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 a disposition wikitable as seen on [[Riven Mods/Weapon Dispos]]. |
− | -- |
+ | -- @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. |
+ | function p.getRivenDispositionTable(frame) |
− | local |
+ | 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( |
+ | 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|...}}
Quick navigation to submodules:
- Module:Weapons/Conclave/data - data store for Conclave-specific weapon stats
- Module:Weapons/characteristics - for generating advantage/disadvantage text under Characteristic sections of weapon articles based on stored data in Module:Weapons/data
- Module:Weapons/compare - for generating text for side-by-side comparison between two weapons
- Module:Weapons/comptable
- Module:Weapons/csv - for generating CSV output of a subset of weapon stats as stored in Module:Weapons/data
- Module:Weapons/data - main submodule for weapon data store
- Module:Weapons/data/credits - in-progress data store for mapping weapons to their development credits
- Module:Weapons/data/dev - sandbox page for data store
- Module:Weapons/data/validate - data validation scripts
- Module:Weapons/dev - sandbox page
- Module:Weapons/infobox - builds weapon infoboxes
- Module:Weapons/nav - builds weapon navigation box as seen at the bottom of weapon articles
- Module:Weapons/ppdata - preprocessed weapon data containing statistical
- Module:Weapons/preprocess - script for seeding ppdata
- Module:Weapons/ppdata/seeder - archived script for seeding ppdata
- Module:Weapons/testcases - unit test suite for Module:Weapons
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 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's case, Jat Kusar has a base 35% crit chance, but since we are comparing against non-normal attacks, Quassus's Ethereal Daggers will have second highest crit chance (30%) behind Tenet Exec's slam shockwaves (38%). |
22:09, 5 August 2022 (UTC) | ||
Weapon and Attack classes | Refactor and Dev | Planning | Low |
|
21:18, 18 January 2022 (UTC) | ||
ExplosionDelay key
|
Refactor | Planning | Low |
|
22:10, 6 January 2022 (UTC) | ||
Reload key
|
Refactor | Planning | Low |
|
|||
Augments in /data
|
Refactor | New | Low |
|
01:37, 31 May 2021 (UTC) | ||
Data validation | Dev/database | Active | Medium |
Create
|
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
|
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 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 21:18, 18 January 2022 (UTC) update: User:Gigamicro implemented a new 21:35, 19 January 2022 (UTC) update: |
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 |
|
06:05, 3 October 2021 (UTC) | 17:47, 2 November 2021 (UTC) |
Railjack Weapons | Dev/Edit/Database | Long-term support | Medium | User:Cephalon Scientia |
02:32, 6 September 2021 (UTC) update: Added most Railjack turrets and ordnances to 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 |
06:18, 10 August 2021 (UTC) | 03:10, 16 August 2021 (UTC) |
Clean up | Clean up | Completed | Medium | User:Cephalon Scientia |
|
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.:
|
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
20:53, 29 July 2021 (UTC) update: We now use |
01:37, 31 May 2021 (UTC) | 20:53, 29 July 2021 (UTC) |
getAttackValue()
|
Refactor | Completed | Medium | User:Cephalon Scientia |
Refactor
20:53, 29 July 2021 (UTC) update: |
01:37, 31 May 2021 (UTC) | 20:53, 29 July 2021 (UTC) |
p.getRivenDispositionTable()
|
Refactor | Completed | Low |
|
3:42, 21 July 2021 (UTC) | 4:49, 21 July 2021 (UTC) | |
p.buildDamageTypeTable(frame)
|
Refactor | Completed | Low |
|
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.
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
- https://warframe.fandom.com/fr/wiki/Module:Weapons
- https://warframe.fandom.com/es/wiki/M%C3%B3dulo:Weapons
- https://warframe.fandom.com/it/wiki/Modulo:Weapons
- https://warframe.fandom.com/de/wiki/Modul:Weapons
- https://warframe.fandom.com/pt-br/wiki/M%C3%B3dulo:Weapons
- https://warframe.fandom.com/zh-tw/wiki/%E6%A8%A1%E7%B5%84:Weapons
- https://warframe.huijiwiki.com/wiki/%E6%A8%A1%E5%9D%97:Weapons#
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 ( MK1, Prime)).
- Parameters:
Weapon
Weapon table (table)tooltip
If true, adds weapon tooltips, false otherwise; defaults to false (boolean)
- Returns: Wikitext of resultant list (string)
weapons.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
- Weapons/Conclave/data
- Weapons/Conclave/data/doc
- Weapons/Conclave/data/melee
- Weapons/Conclave/data/primary
- Weapons/Conclave/data/secondary
- Weapons/characteristics
- Weapons/characteristics/doc
- Weapons/compare
- Weapons/compare/doc
- Weapons/comptable
- Weapons/comptable/doc
- Weapons/csv
- Weapons/csv/doc
- Weapons/data
- Weapons/data/archwing
- Weapons/data/archwing/doc
- Weapons/data/companion
- Weapons/data/companion/doc
- Weapons/data/credits
- Weapons/data/credits/doc
- Weapons/data/dev
- Weapons/data/doc
- Weapons/data/melee
- Weapons/data/melee/doc
- Weapons/data/misc
- Weapons/data/misc/doc
- Weapons/data/modular
- Weapons/data/modular/doc
- Weapons/data/primary
- Weapons/data/primary/doc
- Weapons/data/railjack
- Weapons/data/railjack/doc
- Weapons/data/secondary
- Weapons/data/secondary/doc
- Weapons/data/validate
- Weapons/data/validate/doc
- Weapons/dev
- Weapons/dev/doc
- Weapons/doc
- Weapons/infobox
- Weapons/infobox/doc
- Weapons/nav
- Weapons/nav/doc
- Weapons/ppdata
- Weapons/ppdata/doc
- Weapons/ppdata/seeder
- Weapons/ppdata/seeder/doc
- Weapons/preprocess
- Weapons/preprocess/doc
- Weapons/testcases
- Weapons/testcases/doc
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°' },
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