🌟現在、鉄壁 鉄壁ヘッドショットには対応済みです。
鉄壁HSは通常HSと同じダメージになります。LMG及びDMR、チャージライフル、ハンマーポイント弾を除き、すべてのダメージ値が一致していることを確認しています。

モジュール:WeaponInfobox

提供:Apex Data
2021年8月5日 (木) 15:43時点におけるMntone (トーク | 投稿記録)による版 (物資投下武器で材料関連でエラーが出る不具合の修正)
ナビゲーションに移動 検索に移動

このモジュールについての説明文ページを モジュール:WeaponInfobox/doc に作成できます

require('Module:Utility/mw.html Extensions')

local p = {}
local cfg = mw.loadData('Module:WeaponInfobox/configuration')

local aw = require('Module:Utility/Library')
local iu = require('Module:Utility/Image')
local nu = require('Module:Utility/Name')
local proto = require('Module:Utility/Prototypes')
local formatter -- lazily initialized
local getArgs -- lazily initialized

local function calcBurstAverageRPS(stat, delay)
	delay = delay or stat.firerate.burst_delay
	return stat.mode.burst / ((stat.mode.burst - 1) / stat.firerate.burst + delay)
end

local function calcRechamberRPS(firerate, rechamber)
	rechamber = rechamber or firerate.single_rechamber
	return 1 / (1 / firerate.single + rechamber)
end

local function createCellInRow(tbl, name)
	return tbl:tag('tr')
		:tag('th')
			:wikitext(name)
			:done()
		:tag('td')
end

local function renderRow(tbl, name, item)
	createCellInRow(tbl, name):wikitext(item)
end

local function renderFormatRow(tbl, name, default, common, rare, epic, opts)
	opts = opts or {}
	opts.separator = opts.separator or ' - '
	local row = tbl:tag('tr')
		:addClassIf(opts.class and type(opts.class) == 'string', opts.class)
	
	if name ~= nil then
		row:tag('th')
			:attrIf(opts.headerAlign and type(opts.headerAlign) == 'string', { align = opts.headerAlign })
			:wikitext(name)
	end

	local first = true
	if default ~= nil then
		first = false
		row:tag('td')
			:addClass('cell-type-number')
			:attrIf(opts.align and type(opts.align) == 'string', { align = opts.align })
			:wikitext(default)
	end
	
	if common ~= nil then
		if not first then
			row:tag('td'):wikitext(opts.separator)
		end
		first = false
		row:tag('td')
			:addClass('cell-type-number')
			:attrIf(opts.align and type(opts.align) == 'string', { align = opts.align })
			:wikitext(formatter:common(common))
	end
	
	if rare ~= nil then
		if not first then
			row:tag('td'):wikitext(opts.separator)
		end
		first = false
		row:tag('td')
			:addClass('cell-type-number')
			:attrIf(opts.align and type(opts.align) == 'string', { align = opts.align })
			:wikitext(formatter:rare(rare))
	end
	
	if epic ~= nil then
		if not first then
			row:tag('td'):wikitext(opts.separator)
		end
		first = false
		row:tag('td')
			:addClass('cell-type-number')
			:attrIf(opts.align and type(opts.align) == 'string', { align = opts.align })
			:wikitext(formatter:epic(epic))
	end
	
	if opts.footer ~= nil then
		row:tag('td')
			:attrIf(opts.footerAlign   and type(opts.footerAlign) == 'string',   { align   = opts.footerAlign }, { align = 'left' })
			:attrIf(opts.footerColspan and type(opts.footerColspan) == 'number', { colspan = opts.footerColspan })
			:wikitext(opts.footer)
	end
	return row
end

local function renderReleaseDate(tbl, cfg, release)
	local r = os.date("*t", release)
	local releaseText = string.format(
		'<time datetime="%04d-%02d-%02dT%02d:00:00.000+0900">%d年%d月%d日 %d時</time>~',
		r.year, r.month, r.day, r.hour, r.year, r.month, r.day, r.hour)
	renderRow(tbl, cfg.name, releaseText)
end

local function renderCategory(tbl, cfg, stat, lang)
	local category = nu.type(stat, lang, 1)
	local item
	if lang == 'ja' then
		item = string.format('[[武器#%s|%s]][[Category:%s]]', category, category, category)
	else
		item = string.format('%s[[Category:%s]]', category, category)
	end
	renderRow(tbl, cfg.name, item)
end

local function renderAmmo(tbl, cfg, stat, lang)
	local ammo = nu.ammo(stat, lang)
	local classSuffix
	if aw.stringstarts(stat, 'special_') then
		classSuffix = 'special'
	else
		classSuffix = stat
	end
	
	local item
	if lang == 'ja' then
		item = string.format(
			'%s [[弾薬#%s|<span class="text-ammo text-ammo-%s">%s</span>]][[Category:%s]]',
			iu.ammo(stat, { size = 24 }),
			ammo,
			classSuffix,
			ammo,
			ammo)
	else
		item = string.format(
			'%s <span class="text-ammo text-ammo-%s">%s</span>[[Category:%s]]',
			iu.ammo(stat, { size = 24 }),
			classSuffix,
			ammo,
			ammo)
	end
	createCellInRow(tbl, cfg.name)
		:addClass('cell-ammo')
		:wikitext(item)
end

local function renderCost(tbl, cfg, stat, ammo)
	if stat == nil or aw.stringstarts(ammo, 'special_') then
		return
	end
	
	local text = formatter:format(
		aw.comma(stat[1]),
		aw.comma(stat[2]),
		aw.comma(stat[3]),
		aw.comma(stat[4]),
		'', ' - ')
	renderRow(tbl, cfg.name, cfg.header .. text)
end

local function renderMode(tbl, cfg, stat)
	local builder = require('Module:Utility/StringBuilder').new()
	if stat.burst > 1 then
		builder:appendFormat(cfg.burst, stat.burst)
		builder:append(cfg.burst_category)
	end
	
	if stat.auto then
		if not builder:isEmpty() then
			builder:append(cfg.separator)
		end
		builder:append(cfg.auto, cfg.auto_category)
	end
	
	if stat.single then
		if not builder:isEmpty() then
			builder:append(cfg.separator)
		end
		builder:append(cfg.single, cfg.single_category)
	end
	
	renderRow(tbl, cfg.name, tostring(builder))
end

local function getDamageText(stat)
	if aw.isNumber(stat.pellet) then
		if aw.isNumberAndGreaterThanZero(stat.damage.charged) then
			return string.format(
				'<span class="text-type-number">%s</span>&thinsp;&times;&thinsp;%d → <span class="text-type-number">%s</span>&thinsp;&times;&thinsp;%d',
				stat.damage.base,
				stat.pellet,
				stat.damage.charged,
				stat.pellet)
		else
			return string.format(
				'<span class="text-type-number">%s</span>&thinsp;&times;&thinsp;%d',
				stat.damage.base,
				stat.pellet)
		end
	else
		if aw.isNumberAndGreaterThanZero(stat.damage.amped) then
			return string.format(
				'<span class="text-type-number">%s</span> <span class="text-separator">/</span> %s <span class="text-type-number">%s</span>',
				stat.damage.base,
				iu.item('シールドセル'),
				stat.damage.amped)
		elseif aw.isNumberAndGreaterThanZero(stat.damage.charged) then
			return string.format(
				'<span class="text-type-number">%s</span> → <span class="text-type-number">%s</span>',
				stat.damage.base,
				stat.damage.charged)
		else
			return string.format('<span class="text-type-number">%s</span>', stat.damage.base)
		end
	end
end

local function renderHeadRow(tbl, name, head, cfg, nolist)
	local hlm1 = 0.2 + 0.8 * head
	local hlm2 = 0.4 + 0.6 * head
	local hlm3 = 0.5 + 0.5 * head
	local opts = { align = 'left', footer = cfg.unit }
	if nolist then
		opts.class = 'no-list-style'
		opts.headerAlign = 'right'
	end
	renderFormatRow(
		tbl,
		name,
		nil, hlm1, hlm2, hlm3,
		opts)
end

local function renderLegsRow(tbl, cfg, stat)
	local text
	if aw.isNumberAndGreaterThanZero(stat.legshot_charged) then
		text = string.format(
			'<span class="text-type-number">%s</span> → <span class="text-type-number">%s</span>%s',
			stat.legshot,
			stat.legshot_charged,
			cfg.unit)
	else
		text = string.format(
			'<span class="text-type-number">%s</span>%s',
			stat.legshot,
			cfg.unit)
	end
	tbl:tag('tr')
		:tag('th')
			:wikitext(cfg.legs)
			:done()
		:tag('td')
			:attr('align', 'left')
			:attr('colspan', 6)
			:wikitext(text)
end

local DamageProto = {
	damage = {
		base     = proto.NumberRange(1),
		headshot = proto.NumberRange(1),
		legshot  = proto.NumberRange(0, 1),
	},
}
local ShatterCapsProto = {
	damage = {
		base     = proto.NumberRange(1),
		headshot = proto.NumberRange(1),
		legshot  = proto.NumberRange(0, 1),
	},
	pellet = proto.NumberRange(1),
}
local damageHopups = {
	{
		name = 'anvil_receiver',
		proto = DamageProto,
		textclass = 'text-rarity-legendary',
		dispclass = 'disp-rarity-legendary',
	},
	{
		name = 'shatter_caps',
		proto = ShatterCapsProto,
		textclass = 'text-rarity-epic',
		dispclass = 'disp-rarity-epic',
	},
}
local function renderDamage(tbl, cfg, stat)
	if not proto.validateTypes(stat, DamageProto) then
		return
	end
	
	local cattext = string.format(cfg.head_category, stat.damage.headshot_charged or stat.damage.headshot)
	local cell = createCellInRow(tbl, cfg.name)
	cell:wikitext(cattext)
		:wikitext(getDamageText(stat))
	
	-- Headshot
	local sprm = stat.damage.skullpiercer_rifling or 1
	local hlmc = stat.damage.headshot_charged or 1
	local intable =
		--tbl:tag('tr'):tag('td'):attr('colspan', 2)
		cell
			:tag('table')
				:addClass('condensedtable')
				:addClass('listtable')
	renderHeadRow(intable, cfg.head, stat.damage.headshot, cfg)
	if sprm > 1 then
		local nameS = iu.hopup('skullpiercer_rifling') .. '&nbsp;'
		renderHeadRow(intable, nameS, sprm, cfg, true)
	elseif hlmc > 1 then
		local nameC = '→&nbsp;'
		renderHeadRow(intable, nameC, hlmc, cfg, true)
	end
	
	-- Legsshot
	renderLegsRow(intable, cfg, stat.damage)
	
	-- [Hop-Up] Anvil Receiver & Shatter Caps
	for _, v in ipairs(damageHopups) do
		if proto.validateTypes(stat[v.name], v.proto) then
			local intblS = cell:tag('div')
				:addClass('tpl-weapon-inbox')
				:addClass(v.dispclass)
				:wikitext(iu.hopup(v.name) .. '&nbsp;')
				:tag('span')
					:addClass('text-rarity')
					:addClass(v.textclass)
					:wikitext(getDamageText(stat[v.name]))
					:done()
				:tag('table')
					:addClass('condensedtable')
					:addClass('listtable')
			renderHeadRow(intblS, cfg.head, stat[v.name].damage.headshot, cfg)
			renderLegsRow(intblS, cfg, stat[v.name].damage)
		end
	end
end

local function renderMultipleFirerateRow(intbl, name, cfg, rps, opts)
	opts = opts or {}
	renderFormatRow(
		intbl,
		name .. cfg.rps.name,
		string.format(cfg.rps.format, aw.roundx(rps[1], 2)),
		string.format(cfg.rps.format, aw.roundx(rps[2], 2)),
		string.format(cfg.rps.format, aw.roundx(rps[3], 2)),
		string.format(cfg.rps.format, aw.roundx(rps[4], 2)),
		{ class = opts.rpsClass or '', separator = cfg.rps.separator, footer = cfg.rps.unit })
	renderFormatRow(
		intbl,
		cfg.rpm.name,
		string.format(cfg.rpm.format, aw.comma(aw.roundx(60 * rps[1], 1))),
		string.format(cfg.rpm.format, aw.comma(aw.roundx(60 * rps[2], 1))),
		string.format(cfg.rpm.format, aw.comma(aw.roundx(60 * rps[3], 1))),
		string.format(cfg.rpm.format, aw.comma(aw.roundx(60 * rps[4], 1))),
		{ class = opts.rpmClass and 'all-secondary text-smaller ' .. opts.rpmClass or 'all-secondary text-smaller', headerAlign = 'right', separator = cfg.rpm.separator, footer = cfg.rpm.unit })
end

local function renderVariableFirerateRow(intbl, name, cfg, rps1, rps2, opts)
	opts = opts or {}
	intbl
		:tag('tr')
			:addClassIf(type(opts.rarity) == 'string', 'row-rarity-first disp-rarity-' .. (opts.rarity or 'common'))
			:addClassIf(type(opts.class) == 'string',  opts.class)
			:tag('th')
				:wikitext(name)
				:done()
			:tag('td')
				:addClass('cell-type-number')
				:attr('align', 'right')
				:wikitext(string.format(cfg.rps.format, rps1))
				:done()
			:tag('td')
				:wikitext('&nbsp;→&nbsp;')
				:done()
			:tag('td')
				:addClass('cell-type-number')
				:attr('align', 'right')
				:wikitext(string.format(cfg.rps.format, rps2))
				:done()
			:tag('td')
				:wikitext(cfg.rps.unit)
				:done()
			:done()
		:tag('tr')
			:addClassIf(type(opts.rarity) == 'string', 'row-rarity-last disp-rarity-' .. (opts.rarity or 'common'))
			:addClass('no-list-style')
			:addClass('text-secondary')
			:addClass('text-smaller')
			:tag('th')
				:attr('align', 'right')
				:wikitext(cfg.rpm.name)
				:done()
			:tag('td')
				:addClass('cell-type-number')
				:wikitext(aw.comma(aw.roundx(60 * rps1, 1)))
				:done()
			:tag('td')
				:wikitext('&nbsp;→&nbsp;')
				:done()
			:tag('td')
				:addClass('cell-type-number')
				:wikitext(aw.comma(aw.roundx(60 * rps2, 1)))
				:done()
			:tag('td')
				:wikitext(cfg.rpm.unit)
end

local function renderFirerateRow(intbl, name, cfg, rps, opts)
	opts = opts or {}
	intbl:tag('tr')
		:addClassIf(type(opts.rarity) == 'string', 'row-rarity-one disp-rarity-' .. (opts.rarity or 'common'))
		:addClassIf(opts.class and type(opts.class) == 'string', opts.class)
		:tag('th')
			:wikitext(name)
			:done()
		:tag('td')
			:addClass('cell-type-number')
			:attr('align', 'right')
			:wikitext(string.format(cfg.rps.format, rps))
			:done()
		:tag('td')
			:wikitext(cfg.rps.unit)
			:done()
		:tag('td')
			:tag('small')
				:addClass('text-secondary')
				:wikitext(cfg.rpm.name)
				:done()
			:done()
		:tag('td')
			:addClass('cell-type-number')
			:attr('align', 'right')
			:tag('small')
				:addClass('text-secondary')
				:wikitext(aw.comma(0.1 * aw.round(600 * rps)))
				:done()
			:done()
		:tag('td')
			:tag('small')
				:addClass('text-secondary')
				:wikitext(cfg.rpm.unit)
end

local function renderFirerateCell(cell, cfg, rps, opts)
	opts = opts or {}
	opts.header = opts.header or ''
	cell:wikitext(string.format(
		opts.header .. '<span class="text-type-number">' .. cfg.rps.format .. '</span>' .. cfg.rps.unit .. '<span class="text-secondary"><small>(<span class="text-type-number">%s</span>' .. cfg.rpm.unit .. '</small></span>',
		rps,
		aw.comma(0.1 * aw.round(600 * rps))))
end

local MultipleNumberProto = {
	proto.NumberRange(0),
	proto.NumberRange(0),
	proto.NumberRange(0),
	proto.NumberRange(0),
}
local AnvilReceiverProto = {
	firerate = {
		single = proto.NumberRange(0),
	},
}
local DeadeyesTempoWithChargeProto = {
	firerate = {
		single_charged = proto.NumberRange(0),
	},
}
local DeadeyesTempoWithRechamberProto = {
	firerate = {
		single_rechamber = proto.NumberRange(0),
	},
}
local function renderFirerate(tbl, cfg, stat)
	local firerate = stat.firerate
	if firerate == nil then
		return
	end
	
	local mode = stat.mode
	local cell = createCellInRow(tbl, cfg.name)
	local rps
	if mode.auto then
		if mode.single then
			local hasAnvil = proto.validateTypes(stat.anvil_receiver, AnvilReceiverProto)
			if hasAnvil or firerate.auto ~= firerate.single then
				local intbl
				if firerate.auto ~= firerate.single then
					intbl = cell:tag('table')
						:addClass('condensedtable')
						:addClass('listtable')
					
					local singleRPS
					if aw.isNumberAndGreaterThanZero(firerate.single_rechamber) then
						singleRPS = calcRechamberRPS(firerate)
					else
						singleRPS = firerate.single
					end
					renderFirerateRow(intbl, cfg.auto, cfg, firerate.auto)
					renderFirerateRow(intbl, cfg.single, cfg, singleRPS)
				else
					intbl = cell:tag('table')
						:addClass('condensedtable')
					renderFirerateRow(intbl, '', cfg, firerate.auto)
				end
				
				if hasAnvil then
					intbl:addClass('raritytable')
					renderFirerateRow(intbl, iu.hopup('anvil_receiver') .. '&nbsp;', cfg, stat.anvil_receiver.firerate.single, { rarity = 'legendary' })
				end
				return
			end
		elseif mode.burst > 1 then
			if type(firerate.auto) == 'table' then
				local newcell = tbl:tag('tr')
					:tag('td')
						:attr('colspan', 2)
				renderFirerateCell(newcell:tag('ul'):tag('li'), cfg, firerate.burst, { header = cfg.burst })
				
				local intbl = newcell:tag('table')
					:addClass('condensedtable')
					:addClass('listtable')
				local average = {
					calcBurstAverageRPS(stat, firerate.burst_delay[1]),
					calcBurstAverageRPS(stat, firerate.burst_delay[2]),
					calcBurstAverageRPS(stat, firerate.burst_delay[3]),
					calcBurstAverageRPS(stat, firerate.burst_delay[4]),
				}
				renderMultipleFirerateRow(intbl, cfg.burst_average, cfg, average, { rpsClass = 'no-list-style', rpmClass = 'no-list-style' })
				renderMultipleFirerateRow(intbl, cfg.auto,          cfg, firerate.auto, { rpmClass = 'no-list-style' })
			else
				local intbl = cell:tag('table')
					:addClass('condensedtable')
					:addClass('listtable')
				local average = calcBurstAverageRPS(stat)
				renderFirerateRow(intbl, cfg.burst,         cfg, firerate.burst)
				renderFirerateRow(intbl, cfg.burst_average, cfg, average, { class = 'no-list-style' })
				renderFirerateRow(intbl, cfg.auto,          cfg, firerate.auto)
			end
			return
		elseif aw.isNumberAndGreaterThanZero(firerate.auto_start) then
			local intbl = cell:tag('table')
				:addClass('condensedtable')
			renderVariableFirerateRow(intbl, '', cfg, firerate.auto_start, firerate.auto)
			
			if aw.isNumberAndGreaterThanZero(firerate.auto_start_turbocharger) then
				intbl:addClass('raritytable')
				renderVariableFirerateRow(intbl, iu.hopup('turbocharger') .. '&nbsp;', cfg, firerate.auto_start_turbocharger, firerate.auto, { rarity = 'legendary' })
			end
			return
		end
		rps = firerate.auto
	elseif mode.single then
		if mode.burst > 1 then
			local intbl = cell:tag('table')
							:addClass('condensedtable')
							:addClass('listtable')
			local average = calcBurstAverageRPS(stat)
			renderFirerateRow(intbl, cfg.burst,         cfg, firerate.burst)
			renderFirerateRow(intbl, cfg.burst_average, cfg, average, { class = 'no-list-style' })
			
			local singleRPS
			if aw.isNumberAndGreaterThanZero(firerate.single_rechamber) then
				singleRPS = calcRechamberRPS(firerate)
			else
				singleRPS = firerate.single
			end
			renderFirerateRow(intbl, cfg.single, cfg, singleRPS)
			return
		end
		
		if proto.validateTypes(firerate.single_rechamber, MultipleNumberProto) then
			local calcRPS = {
				calcRechamberRPS(firerate, firerate.single_rechamber[1]),
				calcRechamberRPS(firerate, firerate.single_rechamber[2]),
				calcRechamberRPS(firerate, firerate.single_rechamber[3]),
				calcRechamberRPS(firerate, firerate.single_rechamber[4]),
			}
			
			local intbl = cell:tag('table'):addClass('condensedtable')
			renderMultipleFirerateRow(intbl, '', cfg, calcRPS)
			return
		elseif aw.isNumberAndGreaterThanZero(firerate.single_charged) then
			local minimum
			local maximum
			if aw.isNumberAndGreaterThanZero(firerate.single_charged_minimum) then
				minimum = calcRechamberRPS(firerate, firerate.single_charged_minimum)
				maximum = calcRechamberRPS(firerate, firerate.single_charged)
			elseif aw.isNumberAndGreaterThanZero(firerate.single_rechamber) then
				minimum = calcRechamberRPS(firerate, firerate.single_rechamber)
				maximum = calcRechamberRPS(firerate, firerate.single_rechamber + firerate.single_charged)
			else
				minimum = firerate
				maximum = calcRechamberRPS(firerate, firerate.single_charged)
			end
			
			local intbl = cell:tag('table'):addClass('condensedtable')
			renderVariableFirerateRow(intbl, '', cfg, minimum, maximum)
			
			if proto.validateTypes(stat.deadeyes_tempo, DeadeyesTempoWithChargeProto) then
				intbl:addClass('raritytable')
				
				local mintempo = calcRechamberRPS(firerate, aw.getAsNumber(stat.deadeyes_tempo.firerate.single_charged_minimum, 0))
				local maxtempo = calcRechamberRPS(firerate, stat.deadeyes_tempo.firerate.single_charged)
				renderVariableFirerateRow(intbl, iu.hopup('deadeyes_tempo') .. '&nbsp;', cfg, mintempo, maxtempo, { rarity = 'epic' })
			end
			return
		elseif aw.isNumberAndGreaterThanZero(firerate.single_rechamber) then
			if proto.validateTypes(stat.deadeyes_tempo, DeadeyesTempoWithRechamberProto) then
				local normal = calcRechamberRPS(firerate, firerate.single_rechamber)
				local tempo  = calcRechamberRPS(firerate, stat.deadeyes_tempo.firerate.single_rechamber)
				local intbl = cell:tag('table')
					:addClass('condensedtable')
					:addClass('raritytable')
				renderFirerateRow(intbl, '',                                     cfg, normal)
				renderFirerateRow(intbl, iu.hopup('deadeyes_tempo') .. '&nbsp;', cfg, tempo, { rarity = 'epic' })
				return
			else
				rps = calcRechamberRPS(firerate)
			end
		else
			rps = firerate.single
		end
	else
		return
	end
	
	if type(rps) == 'table' then
		local intbl = cell:tag('table'):addClass('condensedtable')
		renderMultipleFirerateRow(intbl, '', cfg, rps)
	else
		renderFirerateCell(cell, cfg, rps)
	end
end

local function renderProjectileSpeed(tbl, cfg, projectile_speed, projectile_speed_charged)
	if projectile_speed == nil then
		return
	end
	
	local text
	if projectile_speed == math.huge then
		text = cfg.hitscan
	elseif projectile_speed_charged ~= nil then
		text = string.format(
			'<span class="text-type-number">%s</span> → <span class="text-type-number">%s</span>%s',
			string.format(cfg.format, aw.comma(aw.roundx(0.0254 * projectile_speed, 2))),
			string.format(cfg.format, aw.comma(aw.roundx(0.0254 * projectile_speed_charged, 2))),
			cfg.unit)
	else
		text = string.format(
			'<span class="text-type-number">%s</span>%s',
			string.format(cfg.format, aw.comma(aw.roundx(0.0254 * projectile_speed, 2))),
			cfg.unit)
	end
	renderRow(tbl, cfg.name, text)
end

local function renderDPS(tbl, cfg, stat, lang)
	if stat.firerate == nil then
		return
	end
	
	local sld  = require('Module:Stat/Shield')['removelowprofile']
	local node = require('Module:WeaponInfobox/DPS').renderDPS(stat, sld, lang)
	createCellInRow(tbl, cfg.name)
	tbl:tag('tr'):tag('td'):attr('colspan', 2):node(node)
end

local function renderMagazineRow(intbl, name, mag, rsvmag, cfg, opts)
	opts = opts or {}
	intbl:tag('tr')
		:addClassIf(opts.class and type(opts.class) == 'string', opts.class)
		:tag('th')
			:wikitext(name)
			:done()
		:tag('td')
			:addClass('cell-type-number')
			:attr('align', 'right')
			:wikitext(mag)
			:done()
		:tag('td')
			:wikitext(cfg.unit)
			:done()
		:tag('td')
			:addClass('text-secondary')
			:wikitext('&nbsp;/&nbsp;')
			:done()
		:tag('td')
			:addClass('cell-type-number')
			:attr('align', 'right')
			:wikitext(rsvmag == math.huge and cfg.infinity or rsvmag)
			:done()
		:tag('td')
			:wikitext(cfg.unit)
end

local function calcVirtualMagSize(overheat, firerate, moddedLoader)
	if moddedLoader then
		return 1 + math.floor(1.15 * overheat * firerate)
	else
		return 1 + math.floor(overheat * firerate)
	end
end

local function renderMagazine(tbl, cfg, stat, isModdedLoaderAttachable)
	local magazine = stat.magazine
	local typename = type(magazine)
	if typename == 'table' then
		if isModdedLoaderAttachable then
			local intable = createCellInRow(tbl, cfg.name)
				:tag('table')
					:addClass('condensedtable')
			renderFormatRow(
				intable,
				'',
				magazine[1], magazine[2], magazine[3], magazine[4], { footer = cfg.unit })
			renderFormatRow(
				intable,
				iu.passive('modded_loader') .. '&nbsp;',
				aw.round(1.15 * magazine[1]),
				aw.round(1.15 * magazine[2]),
				aw.round(1.15 * magazine[3]),
				aw.round(1.15 * magazine[4]),
				{ footer = cfg.unit })
		else
			local text = formatter:format(magazine[1], magazine[2], magazine[3], magazine[4], cfg.unit, ' - ')
			renderRow(tbl, cfg.name, text)
		end
	elseif typename == 'number' then
		local text
		if magazine == math.huge then
			if stat.firerate and stat.firerate.auto then
				if proto.validateTypes(stat.overheat, MultipleNumberProto) then
					local intable = createCellInRow(tbl, cfg.name)
						:tag('table')
							:addClass('condensedtable')
					renderFormatRow(
						intable,
						'',
						calcVirtualMagSize(stat.overheat[1], stat.firerate.auto),
						calcVirtualMagSize(stat.overheat[2], stat.firerate.auto),
						calcVirtualMagSize(stat.overheat[3], stat.firerate.auto),
						calcVirtualMagSize(stat.overheat[4], stat.firerate.auto),
						{ footer = cfg.unit })
					renderFormatRow(
						intable,
						cfg.seconds.name,
						string.format(cfg.seconds.format, stat.overheat[1]),
						string.format(cfg.seconds.format, stat.overheat[2]),
						string.format(cfg.seconds.format, stat.overheat[3]),
						string.format(cfg.seconds.format, stat.overheat[4]),
						{ class = 'no-list-style all-secondary text-smaller', headerAlign = 'right', footer = cfg.seconds.unit })
					if isModdedLoaderAttachable then
						renderFormatRow(
							intable,
							iu.passive('modded_loader') .. '&nbsp;',
							calcVirtualMagSize(stat.overheat[1], stat.firerate.auto, true),
							calcVirtualMagSize(stat.overheat[2], stat.firerate.auto, true),
							calcVirtualMagSize(stat.overheat[3], stat.firerate.auto, true),
							calcVirtualMagSize(stat.overheat[4], stat.firerate.auto, true),
							{ footer = cfg.unit })
						renderFormatRow(
							intable,
							cfg.seconds.name,
							string.format(cfg.seconds.format, 1.15 * stat.overheat[1]),
							string.format(cfg.seconds.format, 1.15 * stat.overheat[2]),
							string.format(cfg.seconds.format, 1.15 * stat.overheat[3]),
							string.format(cfg.seconds.format, 1.15 * stat.overheat[4]),
							{ class = 'no-list-style all-secondary text-smaller', headerAlign = 'right', footer = cfg.seconds.unit })
					end
					return
				elseif aw.isNumber(stat.overheat) then
					text = string.format(
						'%s (<span class="text-type-number">%d</span>%s <span class="text-separator">/</span> %s <span class="text-type-number">%d</span>%s)',
						cfg.infinity,
						1 + math.floor(stat.overheat * stat.firerate.auto),
						cfg.unit,
						iu.passive('modded_loader'),
						1 + math.floor(1.15 * stat.overheat * stat.firerate.auto),
						cfg.unit)
				else
					text = cfg.infinity
				end
			else
				text = cfg.infinity
			end
			
		elseif stat.rounds_per_shot and stat.rounds_per_shot.single and stat.rounds_per_shot.single > 1 then
			local times = magazine / stat.rounds_per_shot.single
			local timesText
			if type(cfg.times.format) == 'table' then
				if times > 2 then
					timesText = string.format(cfg.times.format[3], times)
				else
					timesText = cfg.times.format[times]
				end
			else
				timesText = string.format(cfg.times.format, times)
			end
			
			text = string.format(
				'<span class="text-type-number">%d</span>%s <span class="text-secondary"><small>(%s)</small></span>',
				magazine, cfg.unit, timesText)
			
		elseif aw.isNumber(stat.magazine_reserve) then
			if isModdedLoaderAttachable then
				local intbl = createCellInRow(tbl, cfg.name)
					:tag('table')
						:addClass('condensedtable')
				renderMagazineRow(intbl, '', magazine, stat.magazine_reserve, cfg)
				renderMagazineRow(intbl, iu.passive('modded_loader') .. '&nbsp;', aw.round(1.15 * magazine), stat.magazine_reserve, cfg)
				return
			else
				text = string.format(
					'<span class="text-type-number">%d</span>%s <span class="text-separator">/</span> <span class="text-type-number">%s</span>%s',
					magazine,
					cfg.unit,
					stat.magazine_reserve == math.huge and cfg.infinity or stat.magazine_reserve,
					cfg.unit)
			end
			
		else
			text = string.format('<span class="text-type-number">%d</span>%s', magazine, cfg.unit)
		end
		renderRow(tbl, cfg.name, text)
	end
end

local function renderReload(tbl, cfg, reload, attachments, ammo, isModdedLoaderAttachable)
	if reload.full == nil then
		return
	end
	
	local muls = { level1 = 0.963, level2 = 0.933, level3 = 0.9 }
	--if ammo == 'heavy' or ammo == 'special_heavy' then
	--	muls = { level1 = 1, level2 = 0.92, level3 = 0.87 }
	--else
	--	muls = { level1 = 1, level2 = 0.95, level3 = 0.9 }
	--end
	
	local incompatible = not attachments.stock
	if incompatible or ammo == 'shotgun' then
		if reload.tactical ~= nil and reload.tactical ~= reload.full then
			local tacticalText, fullText
			if incompatible then
				tacticalText = tostring(reload.tactical)
				fullText     = tostring(reload.full)
			else
				tacticalText = tostring(muls.level3 * reload.tactical)
				fullText     = tostring(muls.level3 * reload.full)
			end
			
			local intable = createCellInRow(tbl, cfg.name)
				:tag('table')
					:addClass('condensedtable')
					:addClass('listtable')
			renderFormatRow(
				intable,
				cfg.tactical.name .. '&nbsp;',
				string.format(cfg.tactical.format, reload.tactical),
				nil, nil, nil, { footer = cfg.tactical.unit })
			if isModdedLoaderAttachable then
				renderFormatRow(
					intable,
					iu.passive('modded_loader') .. '&nbsp;',
					string.format(cfg.tactical.format, 0.75 * reload.tactical),
					nil, nil, nil, { class = 'no-list-style', headerAlign = 'right', footer = cfg.tactical.unit })
			end
			
			renderFormatRow(
				intable,
				cfg.full.name .. '&nbsp;',
				string.format(cfg.full.format, reload.full),
				nil, nil, nil, { footer = cfg.full.unit })
			if isModdedLoaderAttachable then
				renderFormatRow(
					intable,
					iu.passive('modded_loader') .. '&nbsp;',
					string.format(cfg.full.format, 0.75 * reload.full),
					nil, nil, nil, { class = 'no-list-style', headerAlign = 'right', footer = cfg.full.unit })
			end
		elseif isModdedLoaderAttachable then
			local time
			if incompatible then
				time = reload.full
			else
				time = muls.level3 * reload.full
			end
			local text = string.format(
				cfg.full.format .. cfg.full.unit .. ' <span class="text-separator">/</span> %s ' .. cfg.full.format .. cfg.full.unit,
				time,
				iu.passive('modded_loader'),
				0.75 * time)
			renderRow(tbl, cfg.name, text)
		else
			local time
			if incompatible then
				time = reload.full
			else
				time = muls.level3 * reload.full
			end
			renderRow(tbl, cfg.name, string.format(cfg.full.format, time) .. cfg.full.unit)
		end
	elseif reload.tactical ~= nil and reload.full ~= nil then
		renderRow(tbl, cfg.name, '')
		
		local intable = tbl:tag('tr')
			:tag('td')
				:attr('colspan', 2)
					:tag('table')
						:addClass('condensedtable')
						:addClass('listtable')
		local level1
		if muls.level1 < 1 then
			level1 = string.format(cfg.tactical.format, muls.level1 * reload.tactical)
		else
			level1 = nil
		end
		renderFormatRow(
			intable,
			cfg.tactical.name .. '&nbsp;',
			string.format(cfg.tactical.format, reload.tactical),
			level1,
			string.format(cfg.tactical.format, muls.level2 * reload.tactical),
			string.format(cfg.tactical.format, muls.level3 * reload.tactical),
			{ footer = cfg.tactical.unit })
		if isModdedLoaderAttachable then
			local level1modded
			if muls.level1 < 1 then
				level1modded = string.format(cfg.tactical.format, 0.75 * muls.level1 * reload.tactical)
			else
				level1modded = nil
			end
			renderFormatRow(
				intable,
				iu.passive('modded_loader') .. '&nbsp;',
				string.format(cfg.tactical.format, 0.75 * reload.tactical),
				level1modded,
				string.format(cfg.tactical.format, 0.75 * muls.level2 * reload.tactical),
				string.format(cfg.tactical.format, 0.75 * muls.level3 * reload.tactical),
				{ class = 'no-list-style', headerAlign = 'right', footer = cfg.tactical.unit })
		end
		
		local level1empty
		if muls.level1 < 1 then
			level1empty = string.format(cfg.tactical.format, muls.level1 * reload.full)
		else
			level1empty = nil
		end
		renderFormatRow(
			intable,
			cfg.full.name .. '&nbsp;',
			string.format(cfg.full.format, reload.full),
			level1empty,
			string.format(cfg.full.format, muls.level2 * reload.full),
			string.format(cfg.full.format, muls.level3 * reload.full),
			{ footer = cfg.full.unit })
		if isModdedLoaderAttachable then
			local level1emptymodded
			if muls.level1 ~= 1 then
				level1emptymodded = string.format(cfg.tactical.format, 0.75 * muls.level1 * reload.full)
			else
				level1emptymodded = nil
			end
			renderFormatRow(
				intable,
				iu.passive('modded_loader') .. '&nbsp;',
				string.format(cfg.full.format, 0.75 * reload.full),
				level1emptymodded,
				string.format(cfg.full.format, 0.75 * muls.level2 * reload.full),
				string.format(cfg.full.format, 0.75 * muls.level3 * reload.full),
				{ class = 'no-list-style', headerAlign = 'right', footer = cfg.full.unit })
		end
	end
end

local function renderDraw(tbl, cfg, draw, quickdraw_holster, attachments, ammo)
	if draw == nil then
		return
	end
	
	local muls
	if ammo == 'heavy' or ammo == 'special_heavy' then
		muls = { level2 = 0.92, level3 = 0.87 }
	else
		muls = { level2 = 0.95, level3 = 0.9 }
	end
	
	local incompatible = not attachments.stock
	if incompatible or aw.stringstarts(ammo, "special_") then
		local time
		if incompatible or ammo == 'special_sniper' then
			time = draw
		else
			time = 0.75 * draw
		end
		
		local text
		if quickdraw_holster > 0 then
			text = string.format(
				cfg.format .. cfg.unit .. ' <span class="text-separator">/</span> %s ' .. formatter:epic(cfg.format .. cfg.unit),
				time,
				iu.hopup('quickdraw_holster'),
				quickdraw_holster * time)
		else
			text = string.format(cfg.format, time) .. cfg.unit
		end
		renderRow(tbl, cfg.name, text)
	elseif quickdraw_holster > 0 then
		local intable = createCellInRow(tbl, cfg.name)
			:tag('table')
				:addClass('condensedtable')
		renderFormatRow(
			intable,
			'',
			string.format(cfg.format, draw),
			string.format(cfg.format, 0.85 * draw),
			string.format(cfg.format, 0.80 * draw),
			string.format(cfg.format, 0.75 * draw),
			{ footer = cfg.unit })
		renderFormatRow(
			intable,
			iu.hopup('quickdraw_holster') .. '&nbsp;',
			string.format(cfg.format, quickdraw_holster * draw),
			string.format(cfg.format, 0.85 * quickdraw_holster * draw),
			string.format(cfg.format, 0.80 * quickdraw_holster * draw),
			string.format(cfg.format, 0.75 * quickdraw_holster * draw),
			{ footer = cfg.unit })
	else
		local text = formatter:format(
			string.format(cfg.format, draw),
			string.format(cfg.format, 0.85 * draw),
			string.format(cfg.format, 0.80 * draw),
			string.format(cfg.format, 0.75 * draw),
			cfg.unit, ' - ')
		renderRow(tbl, cfg.name, text)
	end
end

local function renderSpread(tbl, cfg, stat, lang)
	local node = require('Module:WeaponInfobox/Spread').renderSpread(stat.spread, stat.quickdraw_holster and stat.quickdraw_holster.spread, lang)
	createCellInRow(tbl, cfg.name)
	tbl:tag('tr'):tag('td'):attr('colspan', 2):node(node)
end

local function createTable(cfg, stat, lang)
	local isModdedLoaderAttachable = stat.category == 'light_machine_gun' or stat.ammo == 'minigun'
	local tbl = mw.html.create('table')
	renderReleaseDate(tbl, cfg.release, stat.release)
	renderCategory(tbl, cfg.category, stat.category, lang)
	renderAmmo(tbl, cfg.ammo, stat.ammo, lang)
	renderCost(tbl, cfg.cost, stat.cost, stat.ammo)
	renderMode(tbl, cfg.mode, stat.mode)
	renderDamage(tbl, cfg.damage, stat)
	renderFirerate(tbl, cfg.firerate, stat)
	renderProjectileSpeed(tbl, cfg.projectilespeed, stat.projectile_speed, stat.projectile_speed_charged)
	renderMagazine(tbl, cfg.magazine, stat, isModdedLoaderAttachable)
	if stat.time then
		if stat.time.draw then
			renderDraw(tbl, cfg.draw, stat.time.draw, stat.time.quickdraw_holster or 0, stat.attachments, stat.ammo)
		end
		
		if stat.time.reload then
			renderReload(tbl, cfg.reload, stat.time.reload, stat.attachments, stat.ammo, isModdedLoaderAttachable)
		end
	end
	renderDPS(tbl, cfg.dps, stat, lang)
	renderSpread(tbl, cfg.spread, stat, lang)
	return tbl
end

local function renderInfobox(args)
	local lang = args and args.lang or 'ja'
	local cfglang = cfg[lang]
	local stat = mw.loadData('Module:Stat/Weapon')[args.name]
	local div = mw.html.create('div')
		:addClass('tpl-infobox-right')
		:addClass('tpl-weapon')
	if aw.stringstarts(stat.ammo, "special_") then
		div:addClass('tpl-weapon-special')
	else
		div:addClass('tpl-weapon-' .. stat.ammo)
	end
	
	div:tag('div')
		:addClass('tpl-weapon-header')
		:wikitext(args.name .. iu.ammo(stat.ammo, { size = 40 }))
	div:node(createTable(cfglang, stat, lang))
	return div
end

function p._main(args, frame)
	formatter = require('Module:Utility/Formatter').new(frame)
	return tostring(renderInfobox(args))
end

function p.main(frame)
	if not getArgs then
		getArgs = require('Module:Arguments').getArgs
	end
	args = getArgs(frame)
	return p._main(args, frame)
end

return p