warframe


Stances automates Stance tables.

See User blog:Cephalon Scientia/Module:Stances—A New Look and Personality (Stance Tables Remastered) for a write-up on the origins of this module.

On this Wiki, Stances is used in:

Associated CSS stylesheet is stored on MediaWiki:Stances.css.

Usage

Direct Invocation

{{#invoke:Stances|function|input1|input2|...}}

Adding New Stance Types

Update Module:Stances/data, following the schema for a stance entry.

Adding Support For New Combo Types

  1. In Module:Stances, update COMBO_TYPES table with the name of combo type key to be added to Module:Stances/data.
  2. In buildComboRow(), add a new key-value pair associated with the new combo type for the tooltips/notes.

Product Backlog

Name Type Status Priority Assignee Description Date Issued Last Update
Finishers Dev/database Planning Low

Add support for storing finisher attacks though there is a different animation for multiple body types (bipedal human, quadruped, heavy, MOA, etc.). Some weapons have unique finishers:

06:23, 30 January 2022 (UTC)
Special attacks Database Planning Low

Add additional stance entries for weapons with unique combos. Name key of these entries should be just the weapon name in EN locale.

  • Tatsu Tatsu's slide attack when having at least 1 Soul Swarm charge
  • Zenistar Zenistar's heavy attack throwing a disk
  • Caustacyst Caustacyst's heavy attack throwing corrosive sludge
  • VaykorSydon Vaykor Sydon's block + heavy attack blind (unsure if this can fit in current schema)
06:23, 30 January 2022 (UTC)
Slam and Heavy Slams Dev/database Planning Low

Add support for storing slam attacks and heavy slam attacks. Slam/heavy slams should be shared between melee classes with some exceptions:

06:23, 30 January 2022 (UTC)
Data validation Dev/database Haitus Medium

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

  • Include type checking for each column/attribute
  • Include checking if a table entry has the required keys (the minimum number of keys needed to support basic features in Module:Stances)
  • Include boundary checking for stat values
19:01, 6 September 2021 (UTC)

Finished Issues

Name Type Status Priority Assignee Description Date Issued Completion Date
Update database schema Database/Dev Completed Low User:Cephalon Scientia
  • ✔️ Standardize typings of key-value pairs in /data to avoid all the type checking in the module.
  • ✔️ Add Image key to support mapping combos to animation gifs.
4:06, 23 August 2021 (UTC) 19:52, 6 September 2021 (UTC)

Documentation

Package items

stances._getAttackIcon(attackType, modifier) (function)
Returns the icon for melee attack.
Parameters:
  • attackType Melee attack name (e.g. 'Default' or 'Block') (string)
  • modifier Attack modifier name (e.g. 'Default' or 'Slam') (string)
Returns: The icon's image in wikitext (string)
stances.buildStanceTable(stanceName, comboType) (function)
Builds stance table.
Parameters:
  • stanceName Stance name (string)
  • comboType A specific combo type to display (EN locale) (string; optional)
Returns: Stance table in wikitext (string)
stances.getStanceWeaponList(table}) (function)
Builds list of weapons that can equip a particlar stance mod as seen on Template:StanceWeapons.
Parameter: table} frame Frame object w/ first argument being string stanceName
Returns: Resultant wikitext of comparison list (string)
stances.buildCompTable(displayAllCombos) (function)
Builds PvE stance comparison table. Used on Stance page.
Parameter: displayAllCombos (string)
Returns: Stance comparison table in wikitext (string)
stances.buildStanceGallery(frame) (function)
Builds stance gallery on stance category pages.
Parameter: frame (table)
Returns: Stance (string)

Created with Docbunto

See Also

Code


---	'''Stances''' automates [[Stance]] tables.<br />
--	
--	See [[User blog:Cephalon Scientia/Module:Stances—A New Look and Personality (Stance Tables Remastered)]] 
--	for a write-up on the origins of this module.
--	
--	On this Wiki, Stances is used in:
--	* [[Stance]] and all stance mod articles
--	** See [[Blind Justice]] for an example
--	
--	Associated CSS stylesheet is stored on [[MediaWiki:Stances.css]]. 
--	
--	@module		stances
--	@alias		p
--	@author		[[User:FINNER|FINNER]]
--	@author		[[User:Celphalon Scientia|Celphalon Scientia]]
--	@attribution	[[User:Gigamicro|Gigamicro]]
--	@image		IconStance.png
--	@require	[[Module:Stances/data]]
--	@require	[[Module:Mods/data]]
--	@require	[[Module:Table]]
--	@require	[[Module:Tooltips]]
--	@require	[[Module:Weapons]]
--	@require	[[Module:Arguments]]
--	@release	stable
--	<nowiki>

local p = {}

local StanceData = mw.loadData("Module:Stances/data")
local ModData = mw.loadData("Module:Mods/data")["Mods"]
local Table = require("Module:Table")
local Tooltip = require("Module:Tooltips")
local Weapon = require("Module:Weapons")
local Arguments = require("Module:Arguments")

local DEFAULT_GALLERY_PARAMS = 'captionposition="below" captionalign="center"'..
	'hideaddbutton="true" spacing="small" position="center" bordersize="none"'..
	'bordercolor="#FFFFFF" navigation="true"'

-- Stores the localized names of the different types of combos.
-- Ordering determines what row they appear in stance table (e.g. Neutral combo is first row)
local COMBO_TYPES = {
	"Neutral", "Forward", "Forward Block", "Block", "Heavy", 
	"Slide", "Aerial", "Wall", "Finisher", "Slam", "Heavy Slam", 
	"Damage", "Hits"	-- Last two "combos" are for legend
}

local MELEE_ATTACK_ICONS = {
	["Default"] = {
		["Default"] = {
			Icon = "BlackBasex64.png",
			Link = "Melee#Normal Attack",
			Title = "Default Melee Attack"
		},
		["Block"] = {
			Icon = "BlackBlockx64.png",
			Link = "Melee#Blocking",
			Title = "Block"
		},
		["Heavy"] = {
			Icon = "BlackChargex64.png",
			Link = "Melee#Heavy Attacks",
			Title = "Heavy Attack"
		},
		["Up"] = {
			Icon = "BlackUpx64.png",
			Link = "Maneuvers",
			Title = "Up"
		},
	},
	["Slam"] = {
		["Default"] = {
			Icon = "RedBasex64.png",
			Link = "Melee#Slam_Radial_Damage",
			Title = "Radial Slam"
		},
		["Block"] = {
			Icon = "RedBlockx64.png",
			Link = "Melee#Blocking",
			Title = "Block"
		},
		["Pause"] = {
			Icon = "RedPausex64.png",
			Link = "Maneuvers",
			Title = "Pause"
		},
		["Heavy"] = {
			Icon = "RedChargex64.png",
			Link = "Melee#Heavy Attacks",
			Title = "Heavy Attack"
		},
		["Up"] = {
			Icon = "RedUpx64.png",
			Link = "Maneuvers",
			Title = "Up"
		},
	},
	["360"] = {
		["Default"] = {
			Icon = "BlueBasex64.png",
			Link = "Melee#Normal Attack",
			Title = "360° Attack"
		},
		["Block"] = {
			Icon = "BlueBlockx64.png",
			Link = "Melee#Blocking",
			Title = "Block"
		},
		["Heavy"] = {
			Icon = "BlueChargex64.png",
			Link = "Maneuvers",
			Title = "Heavy Attack"
		},
		["Up"] = {
			Icon = "BlueUpx64.png",
			Link = "Maneuvers",
			Title = "Up"
		},
	},
	["Ranged"] = {
		["Default"] = {
			Icon = "GunbladeShot.svg",
			Link = "Melee",	-- No better page to link to
			Title = "Ranged"
		},
		["Heavy"] = {
			Icon = "HeavyGunbladeShot.svg",
			Link = "Melee#Heavy Attacks",
			Title = "Ranged"
		},
	},
	["Thrust"] = {
		["Default"] = {
			Icon = "PurpleUpx64.png",	-- Placeholder
			Link = "Melee",
			Title = "Thrust",
		}
	},
	["Dash"] = {
		["Default"] = {
			Icon = "PurpleUpx64.png",	-- Placeholder
			Link = "Melee",
			Title = "Dash",
		}
	},
}

---	Returns the icon for melee attack.
--	@function		p._getAttackIcon
--	@param			{string} attackType Melee attack name (e.g. 'Default' or 'Block')
--	@param			{string} modifier Attack modifier name (e.g. 'Default' or 'Slam')
--	@return			{string} The icon's image in wikitext
function p._getAttackIcon(attackType, modifier)
	if (attackType == nil or attackType == '') then attackType = 'Default' end
	if (modifier == nil or modifier == '') then modifier = 'Default' end
	
	assert(MELEE_ATTACK_ICONS[modifier], 'p._getAttackIcon(attackType, modifier): Invalid modifier type "'..modifier..'"')
	local attackicon = 
		assert(MELEE_ATTACK_ICONS[modifier][attackType], 'p._getAttackIcon(attackType, modifier): Invalid attack type "'..attackType..'"')

	return ('[[File:%s|x19px|link=%s|%s|class=icon]]'):format(attackicon['Icon'], attackicon['Link'], modifier..' '..attackType)
end

---	Returns max number of columns the table needs.
--	@function		getMaxCols
--	@param			{table} stanceData Table containing stance data
--	@return			{number} Max number of columns that the stance table needs 
--							 to display all attack inputs without text wrapping
local function getMaxCols(stanceData)
	local maxCols = 0
	for _, k in ipairs(COMBO_TYPES) do
		local v = stanceData[k]
		-- assert(v,'No "'..k..'" table in '..stanceData.StanceName)
		if v then
			local temp = 0
			assert(v['Attacks'],'No Attacks table in '..stanceData.StanceName..'.'..k)
			for j, w in ipairs(v['Attacks']) do
				temp = temp + 1
			end
			if temp > maxCols then
				maxCols = temp
			end
		end
	end
	
	return maxCols
end

---	Returns individual icon in a combo or error if faulty data.
--	@function		buildComboIcon
--	@param			{string} stanceName Stance name
--	@param			{string} comboName Combo name
--	@param			{string} inputNum Attack input number in combo
--	@return			{string} HTML containing the combo icon as well as the combo 
--							 input's damage multipliers, number of hits, and forced procs
local function buildComboIcon(stanceData, iconData)--comboName, inputNum)
	-- TODO: Pass in stanceData instead of rebuilding stanceData each time function is called
	-- TODO: Build resultant string in a table and perform a table.concat when returning
	local stanceName = stanceData.StanceName
	-- local iconData = stanceData[comboName]['Attacks'][inputNum]
	local iconShape = iconData.Shape or 'Default' -- 'Default', 'Heavy', or nil
	local iconType = iconData.Type or 'Default'	-- 'Default',  '360', 'Slam', 'Ranged'
	local iconNote = iconData.Note and '<sup>&thinsp;</sup>[[#Notes|'..iconData.Note..']]' or ''
	local iconHits = iconData.Hits	-- number of hits of unique attack(s)
	local iconDmg = iconData.Dmg	-- damage muliplier of unique attack(s)
	local iconProc = {}				-- any guaranteed procs from attack(s)
	local icon = ''
	local numRows = Table.size(iconHits)	-- for tracking number of rows to be displayed below combo icon
	
	assert(iconHits and iconDmg and iconData)
	
	-- default value is no procs (empty string)
	local tempIconProc = iconData.Procs and iconData.Procs or { '' }
	local tempIconHitTypes = iconData.Types and iconData.Types or { 'Sweep' }
	
	-- adding rows that will sit under combo icon
	for i = 1, numRows do
		local numProcs = 0
		
		-- for hits that have multiple forced procs
		if type(tempIconProc[i]) == 'table' then
			numProcs = Table.size(tempIconProc[i])
			iconProc[i] = tempIconProc[i]
		elseif tempIconProc[i] == '' then
			iconProc[i] = {}
			iconProc[i][1] = ''
		else
			iconProc[i] = {}
			iconProc[i][1] = tempIconProc[i]
			numProcs = 1
		end
		
		local marginTop = 0.4
		if i > 1 then marginTop = 0.8 end
		
		local hitsString = ''
		local dmgString = ''
		
		if iconHits[i] ~= 1 then
			hitsString = iconHits[i]..'x&thinsp;'
		end
		
		if (iconDmg[i] ~= 100) or (iconDmg[i] == 100 and iconHits[i] == 1) then
			dmgString = iconDmg[i]..'%'
		end
		
		-- when there is multiple hits but each hit does 100% of weapon damage
		if iconHits[i] > 1 and iconDmg[i] == 100 then
			hitsString = iconHits[i]..'x&thinsp;'
			dmgString = '100%'
		end
		
		icon = icon..'<div style="position: inherit; margin-top: -'..marginTop..'em;"><b style="font-size: 10px">'
		
		-- Treat sweeping melee attacks/hits as the default so no need to add an additional icon
		if (tempIconHitTypes[i] ~= nil and tempIconHitTypes[i] ~= 'Sweep' and tempIconHitTypes[i] ~= '') then
			icon = icon..'&thinsp;'..p._getAttackIcon('Default', tempIconHitTypes[i])
		end
		
		icon = icon..'&thinsp;'..hitsString..dmgString
		
		for j = 1, numProcs do
			if iconProc[i][j] ~= nil and iconProc[i][j] ~= '' then
				icon = icon..'&thinsp;'..Tooltip.icon(iconProc[i][j], 'DamageTypes')
			end
		end
		
		icon = icon..'</b></div>'
	end
	
	icon = '<div style="text-align: center; vertical-align: top; white-space: nowrap; display: inline-block; padding: 0.15em 0.15em 0;">'
		..p._getAttackIcon(iconShape, iconType)..iconNote..icon..'</div>'
	return icon
end

---	Calculates total damage multiplier (in %) for a combo against a single target.
--	@function		calcTotalDmgMulti
--	@param			{table} comboData Table that contains data for a particular stance combo
--	@return			{number} Total damage multiplier of a particular stance combo as a percentage
local function calcTotalDmgMulti(comboData)
	local multiplierSum = 0
	
	for i, attack in ipairs(comboData["Attacks"]) do
		for j, multi in ipairs(attack["Dmg"]) do
			-- Ignore radial slam portions of combo attacks because they do not hit the 
			-- epicenter of a melee strike (unlike AoE weapons and actual slam attacks 
			-- which do produce two damage instances).
			-- In other words, a combo with a slam attack will only produce one damage instance
			-- on direct hit of a target; the radial slam is not applied on direct hit.
			-- Damage multiplier calculations are only relevant for single target anyways.
			if (attack["Types"] == nil or attack["Types"][j] ~= "Slam") then
				multiplierSum = multiplierSum + (multi * attack["Hits"][j])
			end
		end
	end
	
	return multiplierSum
end

---	Calculates average damage multiplier (in %) per second for a combo against a single target.
--	@function		calcAvgDmgMulti
--	@param			{table} comboData Table that contains data for a particular stance combo
--	@return			{number} Average damage multiplier per second of a particular stance combo
local function calcAvgDmgMulti(comboData)
	if (comboData["Duration"] == nil) then return 0/0 end
	
	local multiplierSum = calcTotalDmgMulti(comboData)
	local multiplierAvg = multiplierSum / comboData["Duration"]
	return multiplierAvg
end

---	Calculates total damage multiplier (in %) from slash procs for a combo
--	assuming no status duration mods/modifiers are added (i.e. 6 second slash proc).
--	@function		calcTotalSlashProcMulti
--	@param			{table} comboData Table that contains data for a particular stance combo
--	@return			{number} Total slash proc multiplier for a particular stance combo as a percentage
local function calcTotalSlashProcMulti(comboData)
	local multiplierTotal = 0
	
	for i, attack in ipairs(comboData['Attacks']) do
		for i, proc in ipairs(attack["Procs"] or {}) do
			-- first check needed for hits that have multiple forced procs
			-- second check needed if only forced proc for hit is Slash
			if ((type(proc) == 'table' and Table.contains(proc, 'Bleeding')) or proc == 'Bleeding') then
				multiplierTotal = multiplierTotal + (attack["Dmg"][i] * attack["Hits"][i])
			end
		end
	end
	
	-- multiplying by 0.35 b/c slash ticks deals 35% of weapon's modded damage
	-- and accounting for 6 ticks of damage
	return multiplierTotal * 0.35 * 6
end

---	Calculates total combo gain for a combo.
--	@function		calcTotalComboGain
--	@param			{table} comboData Table that contains data for a particular stance combo
--	@return			{number} Total combo gain for a particular stance combo
local function calcTotalComboGain(comboData)
	local comboSum = 0
	
	for i, attack in ipairs(comboData['Attacks']) do
		for i, multi in ipairs(attack["Dmg"]) do
			local comboCount = multi / 100
			-- rounding to nearest integer
			comboCount = math.floor(comboCount + 0.5)
			comboSum = comboSum + (comboCount * attack["Hits"][i])
		end
	end
	
	return comboSum
end

---	Calculates average combo gain per second for a combo.
--	@function		calcAvgComboGain
--	@param			{table} comboData Table that contains data for a particular stance combo
--	@return			{string} Average combo gain per second for a particular stance combo (e.g. "5/s")
local function calcAvgComboGain(comboData)
	if (comboData["Duration"] == nil) then return 'N/A' end
	
	local comboSum = calcTotalComboGain(comboData)
	local comboAvg = comboSum / comboData["Duration"]
	-- rounding to nearest hundredth place
	comboAvg = (math.floor(result * 100 + 0.5) / 100)
	return comboAvg..'/s'
end


---	Calculates total Impact, Puncture, or Slash damage multiplier for a combo.
--	@function		calcTotalPhysicalDmgMulti
--	@param			{table} comboData Table that contains data for a particular stance combo
--	@param			{string} physicalDamageType Physical damage type ("Impact", "Puncture", or "Slash")
--	@return			{number} Total Impact, Puncture, or Slash damage multiplier for a particular stance combo as a percentage (e.g. "200")
local function calcTotalPhysicalDmgMulti(comboData, physicalDamageType)
	local dmgToKeyMap = {
		Impact = "ImpactMultiplier",
		Puncture = "PunctureMultiplier",
		Slash = "SlashMultiplier"
	}
	local physicalDamageBonusKey = dmgToKeyMap[physicalDamageType]
	assert(dmgToKeyMap[physicalDamageType] ~= nil, 
		'calcTotalPhysicalDmgMulti(comboData, physicalDamageType): physicalDamageType "'..physicalDamageType..'" is not "Impact", "Puncture", or "Slash"')
	
	local totalPhysicalDmgMulti = 0
	
	for i, attack in ipairs(comboData['Attacks']) do
		for j, physicalDamageMultiplier in ipairs(attack[physicalDamageBonusKey] or {}) do
			-- If element is 1 that means there is no additional physical damage multiplier (100% of the damage or 1x multiplier)
			if (physicalDamageMultiplier > 1) then
				totalPhysicalDmgMulti = totalPhysicalDmgMulti + physicalDamageMultiplier
			end
		end
	end
	
	return totalPhysicalDmgMulti * 100
end

-- TODO: These notes may be redundant as tooltips already have this information; 
-- can safely omit them unless providing readers direct article links are important
-- Required preliminary keys/movements for a combo
local notes = {
	["Neutral"] = 'While Not Moving',
	["Forward"] = 'While Moving',
	["Forward Block"] = 'While [[Melee#Blocking|Blocking]] & Moving',
	["Block"] = 'While [[Melee#Blocking|Blocking]]',
	["Heavy"] = '[[Melee#Heavy Attack|Heavy Attack]]',
	["Slide"] = 'While [[Maneuvers#Sliding|Sliding]]',
	["Aerial"] = 'While [[Maneuvers#Jumping|In Air]]',
	["Wall"] = 'While [[Maneuvers#Wall Latch|Wall Latching]]',
	["Finisher"] = 'On [[Knockdown|Knocked Down]] Enemy',
	["Slam"] = '[[Melee#Slam_Attack|Looking At Ground While In Air]]',
	["Heavy Slam"] = 'Heavy Attack In Midair',
}
-- Tootips for combo names
local title = {
	["Neutral"] = 'Neutral Combo: Performed while standing still or moving sideways/backwards.',
	["Forward"] = 'Forward Combo: Performed while holding any forward movement key.',
	["Forward Block"] = 'Forward Block or Forward Tactical Combo: Performed while holding Block key and moving forward.',
	["Block"] = 'Block or Neutral Tactical Combo: Performed while holding the Block key.',
	["Heavy"] = 'Heavy Attack: Performed by pressing Heavy Attack button.',
	["Slide"] = 'Slide Attack: Performed while sliding.',
	["Aerial"] = 'Aerial Attack: Performed while looking forward and attacking in mid-air.',
	["Wall"] = 'Wall Attack: Performed while wall latching.',
	["Finisher"] = 'Ground Finisher: Performed on knock downed enemies.',
	["Slam"] = 'Slam Attack: Performed by looking at the ground while in mid-air and attacking.',
	["Heavy Slam"] = 'Heavy Slam Attack: Performed by looking at the ground while in mid-air and Heavy Attacking.',
	["Damage"] = 'For table legend',
	["Hits"] = 'For table legend',
}
---	Returns a complete row for a given combo.
--	@function		buildComboRow
--	@param			{string} stanceName Stance name
--	@param			{string} comboType Combo type (e.g. "Neutral" or "Forward")
--	@return			{string} A table row for a particular combo in a stance in wikitext
local function buildComboRow(stanceData, comboType, maxCols)
	local comboData = stanceData[comboType]
	
	local iconcols = {}
	for j = 1, maxCols do
		table.insert(iconcols, '| style="vertical-align:top; text-align:center; border-left:none; border-right:none;" | '
			..(comboData.Attacks[j] and buildComboIcon(stanceData, comboData.Attacks[j]) or '')
		)
	end
	
	return (([=[
|- id="%s"
| style="padding: 0.75em 0.5em; border-bottom: none;" | <span title="%s" style="color: inherit; white-space: nowrap; border-bottom: 2px dotted #808080; cursor: help;">%s</span><br
/><small>(%s)</small>
| style="text-align: center;" | <b>%s%s</b>
%s
| style="padding: 0.75em 0.5em; border-bottom: none;" | %.01f%%/s
| style="padding: 0.75em 0.5em; border-bottom: none;" | %.01fs
| [[File:%s|x90px]]
]=]):format(
		comboData.Name,
		title[comboType], comboType,
		notes[comboType],
		comboData.Name, comboData.FromWeapon and '*' or '',
		table.concat(iconcols, '\n'),
		calcAvgDmgMulti(comboData),
		comboData.Duration or 0/0,
		comboData.Image or 'Panel.png',
	nil):gsub('-nan%W*s','N / A'))
end

---	Builds the stance table's legend.
--	@function		buildStanceLegend
--	@param			{number} colSpan Number of columns that the stance table span
--	@return			{string} Stance legend in wikitext to be placed at end of stance table
local function _buildStanceLegend(colSpan)
	--could have this be one long string and one format call, but this is easier to read (btw chained concats are one operation)
	return ([=[
|-
| colspan="%s" | <div align="center">
%s&nbsp;Default "Sweep" Attack 
%s&nbsp;360°/Spin&nbsp;Attack 
%s&nbsp;Direct&nbsp;and&nbsp;Radial&nbsp;Slam&nbsp;Attack 
%s&nbsp;Ranged&nbsp;Attack 
%s&nbsp;"Thrust"&nbsp;Attack
<div align="right" style="margin-top:-24px"><div class="mw-customtoggle-stance button">View Full Legend</div></div>
<div id="mw-customcollapsible-stance" class="mw-collapsible mw-collapsed">
]=]):format(--head
		colSpan,
		p._getAttackIcon('Default', 'Default'),
		p._getAttackIcon('Default', '360'),
		p._getAttackIcon('Default', 'Slam'),
		p._getAttackIcon('Default', 'Ranged'),
		p._getAttackIcon('Default', 'Thrust')
	)
	..([=[{| style="margin: -0.5em 0em 0.8em 0.2em;"
|+ [[Stance#Melee Combos|Key Inputs]]
| colspan="1" style="text-align:left; padding:0.3em 0.1em 0em;" | <small>%s&nbsp;Basic Melee&nbsp; (PC default LMB or E; XBX, PSN, and NSW default right button)</small>
|-
| colspan="1" style="text-align:left; padding:0.3em 0.1em 0em;" | <small>%s&nbsp;[[Melee#Heavy Attack|Heavy Attack]]&nbsp;(PC default MMB; XBX, PSN, and NSW default left trigger)</small>
|-
| colspan="1" style="text-align:left; padding:0.3em 0.1em 0em;" | <small>%s&nbsp;[[Melee#Blocking|Block]]&nbsp; (PC default RMB; XBX, PSN, and NSW default right trigger)</small>
|-
| colspan="1" style="text-align:left; padding:0.3em 0.1em 0em;" | <small>%s&nbsp;Forward Movement&nbsp; (PC default W; XBX, PSN, and NSW default tilt up right thumbstick)</small>
|}
]=]):format(p._getAttackIcon('Default', ''), p._getAttackIcon('Heavy', ''), p._getAttackIcon('Block', ''), p._getAttackIcon('Up', ''))--keybinds
	..([=[{| style="margin: -0.5em 0em 0.8em 0.2em;"
|+ Multipliers and Hits
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s&nbsp;Attack does double damage&nbsp;</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s&nbsp;Attack hits twice&nbsp;</small>
|}
]=]):format(buildComboIcon(StanceData.__Legend, StanceData.__Legend.Damage.Attacks[1]), buildComboIcon(StanceData.__Legend, StanceData.__Legend.Hits.Attacks[1]))--multipliers
	..([=[{| style="margin: -0.5em 0em 0.8em 0.2em;"
|+ Forced Procs
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s</small>
|-
| colspan="1" style="text-align:left; padding: 0.3em 0.1em 0em;" | <small>%s (front, back, or ground)&nbsp;</small>
|-
| colspan="1" style="text-align:left; padding: 0.15em 0.2em 0.15em;" | <small>%s ([[Conclave|PvP]] only)&nbsp;</small>
|}
]=]):format(--procs
	Tooltip.full('Knockback', 'DamageTypes'),
	Tooltip.full('Weakened', 'DamageTypes'),
	Tooltip.full('Bleed', 'DamageTypes'),
	Tooltip.full('Inaccuracy', 'DamageTypes'),
	Tooltip.full('Knockdown', 'DamageTypes'),
	Tooltip.full('Lifted', 'DamageTypes'),
	Tooltip.full('Stagger', 'DamageTypes'),
	Tooltip.full('Ragdoll', 'DamageTypes'),
	Tooltip.full('Finisher', 'DamageTypes'),
	Tooltip.full('Impair', 'DamageTypes')
)
	..[=[{| style="margin: -0.5em 0em 0.8em 0.2em;"
|+ Melee Attack Explanations
|
*A "sweep attack" is an arcing melee strike that intends to hit an enemy with the edge of the blade (the length of obround melee hitbox).
*A "thrust attack" is a melee strike that intends to hit an enemy with the tip of the blade (the rounded edge of obround melee hitbox).
*A "360/spin attack" is a sliding sweep attack that hits targets around the player (not always in a 360° arc, but is usually above 180°).
*A "direct slam attack" is a melee strike that hits a single target at the epicenter of a slam attack. Affected by the [[Seismic Wave]] mod.
*A "radial slam attack" is the area-of-effect component of the melee slam attack. Affected by the [[Seismic Wave]] mod.
*A "ranged attack" is any melee attack that launches a ranged projectile or can hit targets beyond melee range.
|-
|}
<div align="right" style="margin-top:-24px"><div class="mw-customtoggle-stance button">View Full Legend</div></div>
</div>
<div style="text-align: left;">
*Average Damage Multiplier per second only applies to single-target DPS calculations.
*Note that hits marked as "Radial Slam Attack" will not apply at the epicenter of a single target hit so these will not be accounted for in Avg Dmg Multi/s calculations.
*Combo animation lengths are approximate and are only accurate within a few tenths of a second.
*Each tile marking in stance preview gifs are 2m apart.
*Combos labeled with an asterisk (*) are shared between weapons within the same weapon type.
</div>]=]--footer
end
-- caches the stance legend, in case that's somehow useful
local function buildStanceLegend(colSpan)
	if type(_buildStanceLegend) == 'function' then
		_buildStanceLegend = _buildStanceLegend '|<colSpan>|'
	end
	return (_buildStanceLegend:gsub('|<colSpan>|',colSpan))
end

---	Returns completed table for a give stance.
--	@function		buildStanceTable
--	@param			{string} stanceName Stance name
--	@param[opt]		{string} comboType A specific combo type to display (EN locale); for multiple combo types,
--									   delimit with comma in the order you want combos to be displayed (e.g. "Heavy,Slide")
--	@return			{string} A table for a particular stance in wikitext
local function buildStanceTable(stanceName, comboType)
	local stanceData = StanceData[stanceName]
	assert(stanceData, 'buildStanceTable(): No stance "'..stanceName..'" in [[Module:Stances/data]]')
	local maxCols = getMaxCols(stanceData)
	
	local tRows = { ([=[
{| class="foundrytable" style="font-family:Roboto; text-align:left; max-width:100%%;"
|+ Default Combos of Stance or Weapon ([[Module:Stances/data|📝 Edit Stance Data]])
|-
! style="text-align:center; padding: 0 0.5em;" | [[Stance#Combo Types|Combo]]/Attack
! style="text-align:center; padding: 0 0.5em;" | Name
! style="text-align:center; padding: 0 0.5em;" colspan="%s"| Attack Multipliers and Forced Procs
! style="text-align:center; padding: 0 0.5em;" | Avg Dmg Multi/s
! style="text-align:center; padding: 0 0.5em;" | Length @<br />1.0 [[Attack Speed]]
! style="text-align:center; padding: 0 0.5em;" | Animation
]=]):format(maxCols) }

	for comboType in (comboType or table.concat(COMBO_TYPES,',')):gmatch('%w[%w ]+%w') do
		if stanceData[comboType] then
			table.insert(tRows, buildComboRow(stanceData, comboType, maxCols))
		end
	end
	
	-- 5 additional columns: combo type, name, length, avg dmg multi, and animation 
	table.insert(tRows, buildStanceLegend(maxCols + 5))
	table.insert(tRows, '\n|}')
	
	return table.concat(tRows)
end

---	Builds stance table.
--	@function		p.buildStanceTable
--	@param			{string} stanceName Stance name
--	@param[opt]		{string} comboType A specific combo type to display (EN locale)
--	@return			{string} Stance table in wikitext
function p.buildStanceTable(frame)
	local args = Arguments.getArgs(frame)
	-- First frame argument is stance name and second frame argument is optional, 
	-- representing a specific combo type to display for a one row wikitable
	return buildStanceTable(mw.text.decode(args[1]), args[2])
end

---	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 mw.text.decode(frame.args[1]) or nil
	local Stance = StanceData[stanceName] or
			error('p.getStanceWeaponList(frame): "'..stanceName..'" not found in [[Module:Stances/data]]')
	local pol = ModData[stanceName].Polarity

	local result = {}
	for _, weap in ipairs(Weapon._getMeleeWeapons(Stance.WeaponType, Stance.ConclaveOnly)) do
		-- TODO: If weap.stancePolarity uses shortname of Polarity (e.g. "V" instead of "Madurai"), this comparison
		-- will not properly mark the weapon as being compatible. Somehow need to normalize how we store/retrieve Polarity names.
		table.insert(result, '*'
				..Weapon._statFormat(weap, nil, 'Name')..(weap.StancePolarity == pol and '&nbsp;✓' or '')
			)
	end
	return table.concat(result, '\n')
end

---	Builds PvE stance comparison table. Used on [[Stance]] page.
--	@function		p.buildCompTable
--	@param			{string} displayAllCombos
--	@return			{string} Stance comparison table in wikitext
function p.buildCompTable(frame)
	local displayAllCombos
	if string.upper(frame.args[1]) == 'ALL' then
		displayAllCombos = true
	elseif string.upper(frame.args[1]) == 'UNIQUE' then
		displayAllCombos = false
	else
		error('p.buildCompTable(frame): Invalid argument "'..frame.args[1]..'"; must be "all" for all combos or "unique" for only unique combos per stance')
	end
	
	-- only show these combos if ignoring shared combos between melee classes
	-- local uniqueCombos = { Neutral=true, Forward=true, Block=true, ['Forward Block']=true }
	
	local tHeader = ([=[
{| class="foundrytable listtable sortable" style="text-align:left; max-width:100%%; font-size:12px;"
|-
! style="text-align:center; padding: 0 0.5em;" | Stance<br/>Name
! style="text-align:center; padding: 0 0.5em;" | Weapon<br/>Type
! style="text-align:center; padding: 0 0.5em;" | [[Melee|Combo]]
! style="text-align:center; padding: 0 0.5em;" | Combo Name
! style="text-align:center; padding: 0 0.5em;" | Length
! style="text-align:center; padding: 0 0.5em;" | Total<br/>Combo<br/>Gain
! style="text-align:center; padding: 0 0.5em;" | Total<br/>Dmg<br/>Multi
! style="text-align:center; padding: 0 0.5em;" | Total<br/>Slash<br/>Proc<br/>Multi
! style="text-align:center; padding: 0 0.5em;" | Total<br/>Multi
! style="text-align:center; padding: 0 0.5em;" | Avg<br/>Dmg<br/>Multi/s
! style="text-align:center; padding: 0 0.5em;" | Total<br />%s Multi
! style="text-align:center; padding: 0 0.5em;" | Total<br />%s Multi
! style="text-align:center; padding: 0 0.5em;" | Total<br />%s Multi
|-]=]):format(
	Tooltip.icon('Impact', 'DamageTypes'),
	Tooltip.icon('Puncture', 'DamageTypes'),
	Tooltip.icon('Slash', 'DamageTypes')
)
	
	local tRows = {}
	
	for _, stanceData in pairs(StanceData) do
		local stanceName = stanceData["StanceName"]
		-- omitting PvP stances in comparison of PvE stances
		if stanceName ~= "__Legend" and not stanceData["ConclaveOnly"] then
			for _, comboType in pairs(COMBO_TYPES) do
				local combo = stanceData[comboType]
				if combo and (displayAllCombos or not combo.FromWeapon) then
					local duration = combo["Duration"] or 'N/A'
					local weaponType = stanceData["WeaponType"] or stanceName
					local totalDmgMulti = calcTotalDmgMulti(combo)
					local slashProcDmgMulti = calcTotalSlashProcMulti(combo)
					table.insert(
						tRows, 
						('| [[%s]] || %s || %s || %s || %ss || +%s || %d%% || %d%% || %d%% || %.0f%%/s || %d%% || %d%% || %d%%\n|-')
							:format(
								stanceName,
								weaponType,
								comboType,
								combo["Name"],
								duration,
								calcTotalComboGain(combo),
								totalDmgMulti,
								slashProcDmgMulti,
								totalDmgMulti + slashProcDmgMulti,
								calcAvgDmgMulti(combo),
								calcTotalPhysicalDmgMulti(combo, "Impact"),
								calcTotalPhysicalDmgMulti(combo, "Puncture"),
								calcTotalPhysicalDmgMulti(combo, "Slash")
							)
						)
				end
			end
		end
	end
	
	return tHeader..table.concat(tRows, '\n')..'\n|}'
end

local function getAllStancesSameType(weaponType)
	local stances = {}
	for _, Stance in pairs(StanceData) do
		local stanceName = Stance["StanceName"]
		if Stance["WeaponType"] == weaponType then
			stances[stanceName] = Stance
		end
	end
	return stances
end

	
---	Builds stance gallery on stance category pages.
--	@function			p.buildStanceGallery
--	@param				{table} frame
--	@return				{string} Stance
function p.buildStanceGallery(frame)
	local weaponType = frame.args ~= nil and mw.text.decode(frame.args[1]) or frame
	local Stances = getAllStancesSameType(weaponType)
	local gallery = {([=[
==%s [[Stance|Stance Mods]]==
<gallery %s>
	]=]):format(weaponType, DEFAULT_GALLERY_PARAMS)
	}
	for _, Stance in pairs(Stances) do
		local stanceName = Stance["StanceName"]
		local modTable = ModData[stanceName]
		-- stances that are used when no stance mod is equipped will not be in M:Mods/data
		if (modTable ~= nil) then
			local galleryEntry = ('File:%s|150px|link=%s|[[%s]]'):format(modTable["Image"], stanceName, stanceName)
			
			if Stance.ConclaveOnly then
				galleryEntry = galleryEntry..'<br/>([[Conclave]] only)'
			end
			
			table.insert(gallery, galleryEntry)
		end
		
	end
	table.insert(gallery, '</gallery>')
	gallery = table.concat(gallery, '\n')
	return frame:preprocess(gallery)
end

return p