WARFRAME Wiki
Advertisement
WARFRAME Wiki

Documentation for this module may be created at Module:Enemies/infobox/doc

--	<nowiki>
local Math = require([[Module:Math]])
local Tooltip = require([[Module:Tooltips]])
local Version = require([[Module:Version]])
local InfoboxBuilder = require([[Module:InfoboxBuilder]])
local DropData = mw.loadData([[Module:DropTables/data]])
local WeaponData = require([[Module:Weapons/data]])
local EnemyData = require([[Module:Enemies/data]])
local DamageTypesData = mw.loadData([[Module:DamageTypes/data]])
local DamageTypes = require([[Module:DamageTypes]])
local Table = require([[Module:Table]])

-- TODO: Move these values to their respective /data pages
local health_vals = {
	["Grineer"] = {
		f1_coef = 0.0150,
		f1_expo = 2.12,
		f2_coef = 10.733,
		f2_expo = 0.72,
	},
	["Corpus"] = {
		f1_coef = 0.0150,
		f1_expo = 2.12,
		f2_coef = 13.416,
		f2_expo = 0.55,
	},
	["Infestation"] = {
		f1_coef = 0.0225,
		f1_expo = 2.12,
		f2_coef = 16.100,
		f2_expo = 0.72,
	},
	["Orokin"] = {
		f1_coef = 0.0150,
		f1_expo = 2.10,
		f2_coef = 10.733,
		f2_expo = 0.685,
	},
}
health_vals["Kuva Grineer"] = health_vals["Grineer"]
health_vals["Corpus Amalgam"] = health_vals["Corpus"]
health_vals["Infested"] = health_vals["Infestation"]
health_vals["Infested Deimos"] = health_vals["Infestation"]
-- This is the default case
setmetatable(health_vals, {
	__index = function () return {
		f1_coef = 0.0150,
		f1_expo = 2.00,
		f2_coef = 10.733,
		f2_expo = 0.5,
	} end
})

local shield_vals = {
	["Grineer"] = {
		f1_coef = 0.0200,
		f1_expo = 1.75,
		f2_coef = 1.6000,
		f2_expo = 0.75,
	},
	["Corpus"] = {
		f1_coef = 0.0200,
		f1_expo = 1.76,
		f2_coef = 2.0000,
		f2_expo = 0.76,
	},
	["Infestation"] = {
		f1_coef = 0.0200,
		f1_expo = 1.75,
		f2_coef = 1.6000,
		f2_expo = 0.75,
	},
}
shield_vals["Kuva Grineer"] = shield_vals["Grineer"]
shield_vals["Corpus Amalgam"] = shield_vals["Corpus"]
shield_vals["Infested"] = shield_vals["Infestation"]
shield_vals["Infested Deimos"] = shield_vals["Infestation"]
setmetatable(shield_vals, {
	__index = function () return {
		f1_coef = 0.0200,
		f1_expo = 1.75,
		f2_coef = 2.0000,
		f2_expo = 0.75,
	} end
})

local armor_vals = {}
setmetatable(armor_vals, {
	__index = function () return {
		f1_coef = 0.0050,
		f1_expo = 1.75,
		f2_coef = 0.4000,
		f2_expo = 0.75,
	} end
})

local overguard_vals = {}
setmetatable(overguard_vals, {
	__index = function () return {
		f1_coef = 0.0015,
		f1_expo = 4.00,
		f2_coef = 260.00,
		f2_expo = 0.90,
	} end
})

local function concatif(stat, str)
	return stat and stat..str
end

---	Builds infobox group for enemy attacks.
--	@function		Infobox:attackGroup
--	@param			{InfoboxBuilder} Infobox InfoboxBuilder object reference
--	@param			{table} enemyData Enemy entry as seen in M:Enemies/data
local function attackGroup(Infobox, enemyData)
	local damageHorizGroup, elems, total, highestDmgDistr, highestDmgDistrType, attackData
	
	for i, attackData in ipairs(enemyData.Stats.Attacks or {}) do
		attack = 'Attack'..i
		
		elems = {}
		highestDmgDistr = -1
		for damageType, distr in pairs(attackData.DamageDistribution) do
			if highestDmgDistr < distr then
				highestDmgDistr = distr
				highestDmgDistrType = damageType
			end
			if damageType ~= 'Impact' and damageType ~= 'Puncture' and damageType ~= 'Slash' then
				table.insert(elems, damageType)
			end
		end
		
		total = attackData.TotalDamage

		damageHorizGroup = mw.html.create('group'):attr('layout', 'horizontal')
			:row(attack..'Impact', nil, attackData.DamageDistribution.Impact and 
					Tooltip.icon('Impact', 'DamageTypes', true)..Math.formatnum(attackData.DamageDistribution.Impact * total), 'impact')
			:row(attack..'Puncture', nil, attackData.DamageDistribution.Puncture and 
					Tooltip.icon('Puncture', 'DamageTypes', true)..Math.formatnum(attackData.DamageDistribution.Puncture * total), 'puncture')
			:row(attack..'Slash', nil, attackData.DamageDistribution.Slash and 
					Tooltip.icon('Slash', 'DamageTypes', true)..Math.formatnum(attackData.DamageDistribution.Slash * total), 'slash')

		for _, elem in ipairs(elems) do
			damageHorizGroup:row(attack..elem, nil, attackData.DamageDistribution[elem] and 
					Tooltip.icon(elem, 'DamageTypes', true)..Math.formatnum(attackData.DamageDistribution[elem] * total), elem)
		end

		local multishot = attackData.Multishot or 1
		
		Infobox:group():header(attackData.AttackName)
			:node(damageHorizGroup)
			:row(attack..'Total', '[[Damage|%s]]', highestDmgDistr == 1 and Math.formatnum(total * multishot)..'[[Category:'..highestDmgDistrType..' Damage Enemies]]' or ('%s (%s%s%%)[[Category:%s Damage Enemies]]'):format(Math.formatnum(total * multishot), Tooltip.icon(highestDmgDistrType, 'DamageTypes', true), Math.round(100 * highestDmgDistr, 0.01), highestDmgDistrType), 'total-damage')
			:row(attack..'BurstCount', '%s', attackData.BurstCount, 'burst-count')
			:row(attack..'ChargeTime', '[[Fire Rate#Charged Weapons|%s]]', concatif(attackData.ChargeTime, ' s'), 'charge-time')
			-- As of 35.0.9, enemies no longer deal critical damage. 
			--:row(attack..'CritChance', '[[Critical Hit|%s]]', attackData.CritChance and Math.round(100 * attackData.CritChance, 0.01)..'%', 'crit-chance')
			--:row(attack..'CritMultiplier', '[[Critical Hit|%s]]', concatif(attackData.CritMultiplier, 'x'), 'crit-multiplier')
			:row(attack..'Falloff', '[[Damage Falloff|%s]]', attackData.Falloff and ('100%% damage up to %s m<br />%.0f%% damage at %s m<br />%.0f%% max reduction'):format(attackData.Falloff.StartRange, 100 * (1 - (attackData.Falloff.Reduction or 1)), attackData.Falloff.EndRange, 100 * (attackData.Falloff.Reduction or 1)), 'damage-falloff')
			:row(attack..'Multishot', '[[Multishot|%s]]', attackData.Multishot and ('%d (%s damage per projectile)'):format(attackData.Multishot, Math.round(total, 0.01)), 'multishot')
			:row(attack..'Range', '%s', concatif(attackData.Range, ' m'), 'range')
			:row(attack..'Magazine', '[[Ammo#Magazine Capacity|%s]]', attackData.Magazine, 'magazine-size')
			:row(attack..'Reload', '[[Reload|%s]]', concatif(attackData.Reload, ' s'), 'reload-time')
			:row(attack..'StatusChance', '[[Status Chance|%s]]', attackData.StatusChance and Math.round(100*attackData.StatusChance, 0.01)..'%', 'status-chance')
			:row(attack..'ShotSpeed', '[[Projectile Speed|%s]]', concatif(attackData.ShotSpeed, ' m/s'), 'projectile-speed')
			:row(attack..'ShotType', '%s', attackData.ShotType, 'projectile-type')
		:done()
	end
end

return {
buildInfobox = function(frame)
	local args = frame.args
	local name = mw.text.decode(args['Name'])
	
	-- In the case of Stalker, error occurred because its name was same as the faction name
	local enemyData
	if name ~= "Stalker" then
		enemyData = Table.deepCopy(EnemyData[name])
	else
		enemyData = require([[Module:Enemies/data/stalker]])["Stalker"]
	end
	
	-- If no enemy data don't return anything
	if not enemyData then return end
	
	-- TODO: Move all the prep before constructing a new Infobox object into a separate local helper function
	-- TODO: Not sure if missionNames should have an equivalent key in the enemy data. Adding this new table since
	-- some usages of T:Enemy uses mission arg for the mission name instead of mission type for assassination targets.
	local planets, tileSets, missionNames, missions, weapons, abilities, multis, procs = {}, {}, {}, {}, {}, {}, {}, {}
	
	for _, planet in ipairs(enemyData.General.Planets or {}) do
		table.insert(planets, '[['..planet..']]')
	end
	for _, tileSet in ipairs(enemyData.General.TileSets or {}) do
		table.insert(tileSets, '[['..tileSet..']]')
	end
	for _, mission in ipairs(enemyData.General.Missions or {}) do
		table.insert(missions, '[['..mission..']]')
	end
	for _, weapon in ipairs(enemyData.General.Weapons or {}) do
		table.insert(weapons, WeaponData[weapon] and Tooltip.full(weapon, 'Weapons') or weapon)
	end
	for _, ability in ipairs(enemyData.General.Abilities or {}) do
		table.insert(abilities, '[['..ability..']]')
	end
	for _, multi in ipairs(enemyData.Stats.Multis or {}) do
		table.insert(multis, multi)
	end
	for _, proc in ipairs(enemyData.Stats.ProcResists or {}) do
		table.insert(procs, Tooltip.full(proc, 'DamageTypes'))
	end
	table.sort(planets)
	table.sort(tileSets)
	table.sort(missions)
	table.sort(weapons)
	table.sort(abilities)
	table.sort(multis)
	table.sort(procs)

	local faction, overguard, shield, health, armor, affinity, baseLevel, spawnLevel
	overguard = (args['Overguard'] or ''):gsub(',', '')	-- TODO: Replace thousands delimiter replacement once we add all enemy data to M:Enemies/data
	shield = (args['Shields'] or ''):gsub(',', '')
	health = (args['Health'] or ''):gsub(',', '')
	armor = (args['Armor'] or ''):gsub(',', '')
	affinity = (args['Affinity'] or ''):gsub(',', '')

	faction = args['Faction'] ~= '' and args['Faction'] or enemyData.General.Faction or ''
	shield = tonumber(shield) or enemyData.Stats.Shield or 0
	health = tonumber(health) or enemyData.Stats.Health or 0
	armor = tonumber(armor) or enemyData.Stats.Armor or 0
	overguard = tonumber(overguard) or enemyData.Stats.Overguard or 0
	affinity = tonumber(affinity) or enemyData.Stats.Affinity or 0
	baseLevel = tonumber(args['BaseLevel']) or enemyData.Stats.BaseLevel or 1
	spawnLevel = tonumber(args['SpawnLevel']) or enemyData.Stats.SpawnLevel or baseLevel

	-- [[MediaWiki:EnemyInfoboxSlider.js]] will try to update all ids at once so adding
	-- hidden empty rows
	local vals = {
		(not(faction) or faction == '') and '<span id="faction" style="display:none"></span>' or '',
		(not(affinity) or affinity == 0) and '<span id="affinity" style="display:none">0</span>' or '',
		(not(overguard) or overguard == 0) and '<span id="overguard" style="display:none">0</span>' or '',
		(not(shield) or shield == 0) and '<span id="shield" style="display:none">0</span>' or '',
		(not(health) or health == 0) and '<span id="health" style="display:none">0</span>' or '',
		(not(armor) or armor == 0) and '<span id="armor" style="display:none">0</span>' or '',
		(not(armor) or armor == 0) and '<span id="damage_redux" style="display:none">0</span>' or '',
		(not(baseLevel) or baseLevel == 0) and '<span id="base_level" style="display:none">0</span>' or '',
		(not(spawnLevel) or spawnLevel == 0 or spawnLevel == baseLevel) and '<span id="spawn_level" style="display:none">0</span>' or '',
		'<span id="slider_max" style="display:none">500</span>',

		'<span id="health_f1_coef" style="display:none">'..health_vals[faction].f1_coef..'</span>',
		'<span id="health_f1_expo" style="display:none">'..health_vals[faction].f1_expo..'</span>',
		'<span id="health_f2_coef" style="display:none">'..health_vals[faction].f2_coef..'</span>',
		'<span id="health_f2_expo" style="display:none">'..health_vals[faction].f2_expo..'</span>',

		'<span id="shield_f1_coef" style="display:none">'..shield_vals[faction].f1_coef..'</span>',
		'<span id="shield_f1_expo" style="display:none">'..shield_vals[faction].f1_expo..'</span>',
		'<span id="shield_f2_coef" style="display:none">'..shield_vals[faction].f2_coef..'</span>',
		'<span id="shield_f2_expo" style="display:none">'..shield_vals[faction].f2_expo..'</span>',

		'<span id="armor_f1_coef" style="display:none">'..armor_vals[faction].f1_coef..'</span>',
		'<span id="armor_f1_expo" style="display:none">'..armor_vals[faction].f1_expo..'</span>',
		'<span id="armor_f2_coef" style="display:none">'..armor_vals[faction].f2_coef..'</span>',
		'<span id="armor_f2_expo" style="display:none">'..armor_vals[faction].f2_expo..'</span>',

		'<span id="overguard_f1_coef" style="display:none">'..overguard_vals[faction].f1_coef..'</span>',
		'<span id="overguard_f1_expo" style="display:none">'..overguard_vals[faction].f1_expo..'</span>',
		'<span id="overguard_f2_coef" style="display:none">'..overguard_vals[faction].f2_coef..'</span>',
		'<span id="overguard_f2_expo" style="display:none">'..overguard_vals[faction].f2_expo..'</span>',
	}

	local mods, resources, relics, blueprints, missionDrops, sigils, items, pigments, others = {}, {}, {}, {}, {}, {}, {}, {}, {}
	local enemyDrops = DropData.Enemies[name]
	
	-- Proliferating drop lists from each possible drop table that an enemy can have
	-- TODO: Refactor drop list builder in a local function
	-- TODO: This can be refactored into Module:DropTables since that module contains definitions of
	-- item table entries via constants (e.g. ITEM_CHANCE_COL)
	if (enemyDrops and not enemyDrops._IgnoreEntry) then
		-- Item type to module name (e.g. 'Mod' to 'Mods' for Module:Mods)
		local itemTypeModuleMap = {
			Mod = 'Mods',
			Resource = 'Resources',
			Arcane = 'Arcane',
			Relic = 'Void',
			Sigil = 'Sigils',
			Blueprint = 'Blueprints',
		}
		for _, mod in ipairs(enemyDrops.Mods or {}) do
			local tooltip = Tooltip.full(mod[1], itemTypeModuleMap[mod[2]])
			table.insert(mods, ('%s&nbsp;%0.2f%%')
					:format(
						mod[4] and mod[4]..'&nbsp;'..tooltip or tooltip,
						enemyDrops.ModChance * mod[3] / 100)
					)
		end
		for _, resource in ipairs(enemyDrops.Resources or {}) do
			table.insert(resources, ('%s%s&nbsp;%0.2f%%')
					:format(
						resource[4] and resource[4]..'&nbsp;' or '',
						require('Module:Tooltips/icon')['Resources'](resource[1]) and Tooltip.full(resource[1], 'Resources') or '[['..resource[1]..']]',
						enemyDrops.ResourceChance * resource[3] / 100)
					)
		end
		for _, relic in ipairs(enemyDrops.Relics or {}) do
			table.insert(relics, ('%s&nbsp;%0.2f%%'):format(
				Tooltip.full(relic[1], 'Void'),
				enemyDrops.RelicChance * relic[3] / 100)
			)
		end
		for _, blueprint in ipairs(enemyDrops.Blueprints or {}) do
			table.insert(blueprints, ('%s&nbsp;%0.2f%%'):format(
				blueprint[1],
				enemyDrops.BlueprintChance * blueprint[3] / 100)
			)
		end
		for _, sigil in ipairs(enemyDrops.Sigils or {}) do
			table.insert(sigils, ('%s&nbsp;%0.2f%%')
					:format(
						Tooltip.full(sigil[1], itemTypeModuleMap[sigil[2]]) or sigil[1],
						enemyDrops.SigilChance * sigil[3] / 100)
					)
		end
		for _, item in ipairs(enemyDrops.Items or {}) do
			table.insert(items, ('%s&nbsp;%0.2f%%')
					:format(
						item[2]~='Item' and Tooltip.full(item[1], itemTypeModuleMap[item[2]]) or item[1],
						enemyDrops.ItemChance * item[3] / 100)
					)
		end
		for _, pigment in ipairs(enemyDrops.Pigments or {}) do
			table.insert(pigments, ('%sx&nbsp;%s&nbsp;%0.2f%%')
					:format(
						pigment[4],
						pigment[2]~='Item' and Tooltip.full(pigment[1], itemTypeModuleMap[pigment[2]]) or pigment[1],
						enemyDrops.PigmentChance * pigment[3] / 100)
					)
		end
	end
	
	local Infobox = InfoboxBuilder('WARFRAME Wiki:L10n/general.json', 'WARFRAME Wiki:L10n/meta.json', 'WARFRAME Wiki:L10n/weapons.json'):attr('type', 'enemyBox')
		:tag('title')
			:tag('default')
				:tag('b'):wikitext(name..'[[Category:Enemies]]'):done()
			:done()
		:done()
		:tag('image'):attr('source', 'Image')
			:tag('default'):wikitext(enemyData.General.Image or 'UnidentifiedItem.png'):done()
		:done()
		:group()
			:caption('CodexSecret', enemyData.General.CodexSecret and '[[Codex|%s]][[Category:Codex Secret]]' or nil, 'codex-secret')
			:caption('UpdateInfoboxData', '[[Module:Enemies/data|📝 %s]]', 'update-infobox-data')
			:caption('Description', enemyData.General.Description)
		:done()

		:group():header('%s', 'general-information')
			:srow('Faction', '[[Faction|%s]]', 'faction', faction,
				enemyData.General.Faction~='' and '[[Category:'..enemyData.General.Faction..']]', 'faction')
			:row('Planets', '[[Star Chart|Planet(s)]]', (planets and planets[1]) and table.concat(planets, '<br />'))
			:row('MissionNames', '[[Mission|Mission Name(s)]]', (missionNames and missionNames[1]) and table.concat(missionNames, '<br />'))
			:row('Missions', '[[Mission|Mission Type(s)]]', (missions and missions[1]) and table.concat(missions, '<br />'))
			:row('TileSets', '[[Tile Sets|Tile Set(s)]]', (tileSets and tileSets[1]) and table.concat(tileSets, '<br />'))
			:row('Type', '%s', enemyData.General.Type, 'type', '[[Category:'..enemyData.General.Type..' Enemies]]')
			:row('Weapons', 'Weapon(s)', (weapons and weapons[1]) and table.concat(weapons, '<br />'))
			:row('Abilities', 'Abilities', (abilities and abilities[1]) and table.concat(abilities, '<br />'))
		:done()

		:group():header('%s', 'statistics')
			:srow('Affinity', '[[Affinity#Enemy Affinity Scaling|Affinity]]', 'affinity', affinity)

			:srow('Overguard', '[[Overguard]]', 'overguard', overguard, '[[Category:Overguard Enemies]]')
			:caption('OverguardT', overguard and overguard ~= 0 and DamageTypes.healthMod('Overguard') or nil)

			:srow('Shields', '[[Shield]]', 'shield', shield)
			:srow('Health', '[[Health]]', 'health', health)
			:srow('Armor', '[[Armor]]', 'armor', armor)
			:srow('DmgReduction', '[[Damage Reduction|Dmg. Reduction]]', 'damage_redux', armor and Math.round(math.sqrt(3 * armor), 0.01), '%')
			:caption('Resists', DamageTypes.healthMod(enemyData.General.FactionDamageOverride or enemyData.General.Faction))

			:row('Bleedout', '[[Bleedout]]', concatif(enemyData.Stats.Bleedout,' s'))
			:row('BodyMultis', '[[Enemy Body Parts|Body Multipliers]]', (multis and multis[1]) and table.concat(multis, '<br />'))
			:row('ProcResists', '[[Proc|Proc Immunity]]', (procs and procs[1]) and table.concat(procs, '&nbsp;&nbsp;'))
			:srow('BaseLevel', '[[Enemy Level Scaling#Scaling of Fundamental Stats|Base Level]]', 'base_level', baseLevel)
			:srow('SpawnLevel', '[[Enemy Level Scaling#Scaling of Fundamental Stats|Spawn Level]]', 'spawn_level', (spawnLevel and baseLevel ~= spawnLevel) and spawnLevel or 0)
		:done()
	
	attackGroup(Infobox, enemyData)
	
	Infobox:group()
		:header('Level Scaling')
			:tag('data'):attr('source', 'Slider')
				:tag('default'):wikitext(table.concat(vals)..'<div id="slider_div" class="flex-container" style="align-items: center;">JavaScript not loaded. Please refresh your browser using Ctrl+F5 on PC or Shift+R on Mac.</div>'):done()
			:done()
			:tag('data'):attr('source', 'SelectedLevel')
				:tag('label'):wikitext('{{text||Selected Level|hover=For higher enemy levels input the value manually.|cursor=help}}'):done()
				:tag('default'):wikitext('<span id="out_lvl">&ndash;&ndash;</span><span id="reset_btn"></span>'):done()
			:done()
			:tag('data'):attr('source', 'EHP')
				:tag('label'):wikitext('{{text||EHP|hover=Effective amount of hit points, taking health, armor and shields into account.|cursor=help}}'):done()
				:tag('default'):wikitext('<span id="out_ehp">&ndash;&ndash;</span>'):done()
			:done()
			:tag('data'):attr('source', 'SP_EHP')
				:tag('label'):wikitext('[[The Steel Path|Steel Path]] EHP'):done()
				:tag('default'):wikitext('<span id="out_sp_ehp">&ndash;&ndash;</span>'):done()
			:done()
		:done()
	
		:group():header('%s', 'miscellaneous')
			:row('CodexScans', '[[Codex|Codex Scans]]', enemyData.General.Scans)
			:row('VA', 'Voice Actor', enemyData.General.Actor)
			:row('Introduced', '%s', enemyData.General.Introduced and Version._getVersionLink(enemyData.General.Introduced), 'introduced', enemyData.General.Introduced and Version._getVersionCategory(enemyData.General.Introduced))
		:done()
	
		:group():header('%s', 'drops')
			:caption('NoDrops', true
				and not next(mods)
				and not next(resources)
				and not next(relics)
				and not next(blueprints)
				and not next(missionDrops)
				and not next(sigils)
				and not next(items)
				and not next(others)
				and 'None[[Category:Enemies With No Drops]]' or nil)
			:row('ModDrops', '[[File:Mod TT 20px.png|x12px|link=]] [[Mod|Mod Drops]]', table.concat(mods, '<br />'))
			:row('ResourceDrops', '[[Resources|Resource Drops]]', table.concat(resources, '<br />'))
			:row('RelicDrops', '[[Void Relic|Relic Drops]]', table.concat(relics, '<br />'))
			:row('BPDrops', '[[Foundry|Blueprint/Item Drops]]', table.concat(blueprints, '<br />'))
			:row('ItemDrops', 'Additional Item Drops', table.concat(items, '<br />'))
			:row('MissionDrops', '[[Mission|Mission Drops]]', table.concat(missionDrops, '<br />'))
			:row('SigilDrops', '[[Sigils|Sigil Drops]]', table.concat(sigils, '<br />'))
			:row('PigmentDrops', '[[Pigment|Pigment Drops]]', table.concat(pigments, '<br />'))
			-- Editor override on articles; these drops are not listed in M:DropTables/data
			:row('OtherDrops', 'Other Drops', table.concat(others, '<br />'))
		:done()
		:group():header('%s', 'official-drop-tables')
			:caption('official-drop-tables', 'https://www.warframe.com/droptables', 'official-drop-tables')
		:done()
		
	return frame:preprocess(tostring(Infobox))
end,

-- TODO: Implement this function based on [[Template:EnemyHoriz]] contents
buildHorizontalInfobox = function(frame)
	return error()
end
}
Advertisement