Module:Move7: Difference between revisions

From Wavu Wiki, the 🌊 wavy Tekken wiki
No edit summary
No edit summary
Line 165: Line 165:
local startup = s:match('i(%d+)')
local startup = s:match('i(%d+)')
assert(matches, 'startup is invalid: ' .. s)
assert(startup, 'startup is invalid: ' .. s)
return assert(tonumber(startup), 'not a number: ' .. startup)
return assert(tonumber(startup), 'not a number: ' .. startup)

Revision as of 20:33, 14 September 2023

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

local O = require("Module:O")
local cargo = mw.ext.cargo

local tables = 'MoveDataCargoTest'
local allFields = 'id,name,input,target,damage,reach,tracksLeft,tracksRight,startup,recv,tot,crush,block,hit,ch,notes'

function maxOrNil(a, b)
	if a and b then
		return math.max(tonumber(a) or 0, tonumber(b) or 0)
	elseif a and not b then
		return a
	elseif b and not a then
		return b
	else
		return nil
	end
end

function cargoBoolToBool(str)
	assert(type(str) == 'string')
	
	if str == '1' then
		return true
	else
		return false
	end
end

function boolToCargoBool(b)
	assert(type(b) == 'boolean')
	
	if b then
		return '1'
	else
		return nil
	end
end

local p = {};

-- Get move id for querying, which is the 1st unnamed param of a template.
-- Moves are queried like this: {{TempateName|<moveId>}}
function getQueryId(frame)
	local id = assert(frame:getParent().args[1], '1st unnamed param must be move id')
	return mw.text.trim(id) -- whitespace is stripped only from named params
end

function moveNotFoundMsg(id)
	return "move with id = '" .. id .. "' not found"
end

p.display = function(frame)
	local function appendFrom(dst, src)
		for k, v in pairs(src) do
			dst[k] = v
		end
	end
	local function getLeads(parentId)
		local function concatKeys(t)
			local result = ''
			for k, _ in pairs(t) do
				if result == '' then
					result = result .. k
				else
					result = result .. ', ' .. k
				end
			end
			return result
		end
		
		local leads = {}
		local visitedParents = {}
		
		while parentId and parentId ~= '' do
			visitedParents[parentId] = true
			
			local parent = cargo.query(tables, 'input,target,damage,parent', { where = "id='" .. parentId .. "'" })[1]
			assert(parent, 'parent = "' .. parentId .. '" not found')

			parentId = parent['parent']
			assert(not visitedParents[parentId], 'Found parent cycle: ' .. concatKeys(visitedParents) .. ', ' .. parentId)
			parent['parent'] = nil
			
			for k, v in pairs(parent) do
				local leadName = k .. 'Lead'
				if not leads[leadName] then
					leads[leadName] = ''
				end
				
				leads[leadName] = v .. leads[leadName]
			end
		end
		
		return leads
	end
	
	local leads = getLeads(frame:getParent().args['parent'])
	if not next(leads) then
		return frame:expandTemplate{ title = 'MoveDataCargoTest/Display/Impl', args = frame:getParent().args }
	end
	
	local args = {}
	appendFrom(args, frame:getParent().args)
	appendFrom(args, leads)
	return frame:expandTemplate{ title = 'MoveDataCargoTest/Display/Impl', args = args }
end

p.inherit = function(frame)
	local id = getQueryId(frame)
	local result = cargo.query(tables, allFields .. ',MoveDataCargoTest._pageName=page', { where = "id = '" .. id .. "'" })[1]
	assert(result, moveNotFoundMsg(id))
	
	for k, v in pairs(result) do
		local override = frame:getParent().args[k]
		if override then
			result[k] = override
		end
	end
	
	result[1] = '_table=' .. tables
	frame:callParserFunction{ name = '#cargo_store', args = result }
	result[1] = nil
	
	result['inheritedFrom'] = '[['..result['page']..'#'..id..'|'..id..']]'
	return frame:expandTemplate{ title = 'MoveDataCargoTest/Display', args = result }
end

p.punisherTable = function(frame)
	local function notFoundMsg(type, i, name)
		return type .. ': required parameter punisher['..i..'].' .. name .. ' not found'
	end
	
	local punishersByType = {}
	for _, type in ipairs({
		'standing', 'crouching', 'backTurnedOpponent', 'groundedOpponent'
	}) do
		local punishersEncoded = frame:getParent().args[type]
		if punishersEncoded then
			local punishers = {}
			for i, v in ipairs(O.decode(punishersEncoded)) do
				local decoded = O.decode(v)
				assert(decoded.moveId, notFoundMsg(type, i, 'moveId'))
				
				if decoded.enemy then
					decoded.enemy = tonumber(decoded.enemy)
				end
				if decoded.hard == 'true' then
					decoded.hard = true
				else
					decoded.hard = false
				end
				
				table.insert(punishers, decoded)
			end
			punishersByType[type] = punishers
		end
	end
	return p._punisherTable(frame, punishersByType)
end

p._punisherTable = function(frame, punishersByTypeUnsorted)
	local function queryMoveInfo(punishers)
		local function getFirstStartupFrame(s)
			assert(type(s) == 'string', 'actual type is ' .. type(s))
			
			local startup = s:match('i(%d+)')
			assert(startup, 'startup is invalid: ' .. s)
			
			return assert(tonumber(startup), 'not a number: ' .. startup)
		end
		
		for _, punisher in ipairs(punishers) do
			local enemy = 0
			local input = ''
			local damage = 0
			local hit = nil
			local moveId = punisher.moveId
			while moveId ~= '' do
				local move = cargo.query(tables, 'parent,startup,input,damage,hit', {
					where = "id = '" .. moveId .. "'"
				})[1]
				assert(move, moveNotFoundMsg(moveId))
				
				enemy = -getFirstStartupFrame(move['startup'])
				
				input = move['input'] .. input
				for k, v in string.gmatch(move['damage'], "(%d+)") do
					damage = damage + tonumber(k)
				end
				if not hit then
					hit = move['hit']
				end
				
				id = move['parent']
			end
			
			if not punisher.enemy then
				punisher.enemy = enemy
			end
			if not punisher.input then
				punisher.input = input
			end
			if not punisher.damage then
				punisher.damage = damage
			end
			if not punisher.frames then
				punisher.frames = hit
			end
		end
	end
	local function store(punishers, type, frame)
		for _, punisher in ipairs(punishers) do
			frame:callParserFunction{ name = '#cargo_store', args = {
				'_table=Punisher',
		        type = type,
				enemy = punisher.enemy,
				damage = punisher.damage,
				combo = punisher.combo,
				wallCombo = punisher.wallCombo,
				rageCombo = punisher.rageCombo,
				hard = boolToCargoBool(punisher.hard),
			} }
		end
	end

	local punishersByType = {}
	for type, punishers in pairs(punishersByTypeUnsorted) do
		table.sort(punishers, function(l, r) return l.enemy > r.enemy end)
		queryMoveInfo(punishers)
		store(punishers, type, frame)
		
		table.insert(punishersByType, { type = type, values = punishers })
	end
	
	local displayArgs = {}
	for _, punishers in ipairs(punishersByType) do
		local rowspansByDisadvantage = {}
		for _, punisher in ipairs(punishers.values) do
			local count = rowspansByDisadvantage[punisher.enemy]
			if count then
				rowspansByDisadvantage[punisher.enemy] = count + 1
			else
				rowspansByDisadvantage[punisher.enemy] = 1
			end
		end
		
		local rows = ''
		for _, punisher in ipairs(punishers.values) do
			local row = mw.html.create('tr')
			
			local rowspan = rowspansByDisadvantage[punisher.enemy]
			if rowspan then
				row:tag('td'):wikitext(punisher.enemy):attr('rowspan', rowspan)
				rowspansByDisadvantage[punisher.enemy] = nil
			end
			local inputCell = row:tag('td')
			inputCell:wikitext(punisher.input)
			if punisher.hard then
				inputCell:tag('sup'):wikitext('[hard]')
			end
			local damageCell = row:tag('td')
			damageCell:wikitext(punisher.damage)
			if punisher.combo then
				damageCell:tag('br')
				damageCell:wikitext(punisher.combo .. ' with combo')
			end
			if punisher.wallCombo then
				damageCell:tag('br')
				damageCell:wikitext(punisher.wallCombo .. ' with a wall')
			end
			if punisher.rageCombo then
				damageCell:tag('br')
				damageCell:wikitext(punisher.rageCombo .. ' with Rage')
			end
			row:tag('td'):wikitext(punisher.frames)
			
			rows = rows .. tostring(row)
		end
		
		displayArgs[punishers.type] = rows
	end
	
	return frame:expandTemplate{
		title = 'MoveDataCargoTest/PunisherTable/Display',
		args = displayArgs
	}
end

p.punishment = function(frame)
	local character = frame:getParent().args[1]
		
	--[[
		Returns punishers by type. In each type punishers are sorted from fastest to slowest.
		Table structure: 
		{
			Standing = {
				{ enemy, damages = {
					regular = int,
					wall    = int,
					rage    = int,
					hard    = int,
				} },
				...
			},
			Crouching = {
				...
			},
		}
	--]]
	local function getCharacterPunishersByType(character)
		local punishers = cargo.query(
			'Punisher',
			'type,enemy,damage,combo,wallCombo,rageCombo,hard',
			{ where =
				"Punisher._pageName='User:Lume/Cargo testing/" .. character .. " punishers'"
				.. " AND (type = 'standing' OR type = 'crouching')"
			}
		)
		
		local punishersByType = {}
		for _, p in ipairs(punishers) do
			local type = p.type
			local enemy = -tonumber(p.enemy)
			local hard = cargoBoolToBool(p.hard)
			
			local damages = {
				wall = tonumber(p.wallCombo),
				rage = tonumber(p.rageCombo),
			}
			local regular = maxOrNil(p.damage, p.combo)
			if hard then
				damages.hard = regular
			else
				damages.regular = regular
			end
			
			if not punishersByType[type] then
				punishersByType[type] = {}
			end
			
			local sameDisadvantagePunisherFound = false
			for _, p in ipairs(punishersByType[type]) do
				if p.enemy == enemy then
					sameDisadvantagePunisherFound = true
					
					for dmgType, dmg in pairs(damages) do
						p.damages[dmgType] = maxOrNil(p.damages[dmgType], dmg)
					end
				end
			end
			
			if not sameDisadvantagePunisherFound then
				table.insert(punishersByType[type], { enemy = enemy, damages = damages })
			end
		end
		for _, punishers in pairs(punishersByType) do
			local currentDisadvantages = {}
			for _, punisher in ipairs(punishers) do
				currentDisadvantages[punisher.enemy] = true
			end
			
			-- punishment chart must contain all punishers from -10 to -15
			for i = 10, 15, 1 do
				if not currentDisadvantages[i] then
					table.insert(punishers, { enemy = i, damages = {} })
				end
			end
			
			table.sort(punishers, function(l, r) return l.enemy < r.enemy end)
		end
		
		return punishersByType
	end
	
	local punishersByType = getCharacterPunishersByType(character)
	
	local span = 80
	for _, punishers in pairs(punishersByType) do
		for _, punisher in pairs(punishers) do
			for _, dmg in pairs(punisher.damages) do
				if dmg then
					span = math.max(span, dmg)
				end
			end
		end
	end
	if span % 5 ~= 0 then
		span = span + 5 - (span % 5)
	end
	
	local scale = mw.html.create('div'):addClass('punishment-scale')
	for i = 0, span, 5 do
		local pip = mw.html.create('div')
			:addClass('punishment-scalepip')
			:css("width", string.format("%.2f%%", 100 * i / span))
			:node(mw.html.create('div')
				:addClass('punishment-piplabel')
				:wikitext(i))
		if i % 50 == 0 then
			pip:addClass('major')
		elseif i % 25 == 0 then
			pip:addClass('minor')
		end
		scale:node(pip)
	end
	
	local data = mw.loadData('Module:Fighter/punishment')
	local root = mw.html.create('div'):addClass('punishment')
	for type, punishers in pairs(punishersByType) do
		local group = mw.html.create('div'):addClass('punishment-group')
		root:node(group)
		group:node(mw.html.create("div")
			:addClass('punishment-label')
			:wikitext(type:gsub("^%l", string.upper) .. " punishment"))
		local grid = mw.html.create('div'):addClass('punishment-grid')
		group:node(grid)
		
		local dmgClasses = {
			regular = 'bg-blue',
			rage = 'bg-purple',
			wall = 'bg-orange',
			hard = 'bg-green',
		}
		
		best = {}
		for _, punisher in ipairs(punishers) do
			-- don't carry rage/meter punishment, makes viz. look funky
			best[dmgClasses.rage] = nil
			for dmgType, dmg in pairs(punisher.damages) do
				local dmgClass = dmgClasses[dmgType]
				if not best[dmgClass] or best[dmgClass] < dmg then
					best[dmgClass] = dmg
				end
			end
			if best[dmgClasses.regular] then
				for k, v in pairs(best) do
					if k ~= dmgClasses.regular and best[k] <= best[dmgClasses.regular] then
						best[k] = nil
					end
				end
			end
			
			bestSortedKeys = {}
			for k, _ in pairs(best) do
				table.insert(bestSortedKeys, k)
			end
			-- Larger bars first so that smaller bars are actually visible
			table.sort(bestSortedKeys, function(a, b) return best[a] > best[b] end)
			
			grid:node(mw.html.create('div')
				:addClass('punishment-startup')
				:wikitext("-" .. punisher.enemy))
			local bars = mw.html.create('div'):addClass('punishment-bars')
			grid:node(bars)
			for i, k in ipairs(bestSortedKeys) do
				local class = k
				bars:node(mw.html.create('div')
					:addClass('punishment-bar')
					:addClass(class)
					:css("width", string.format("%.2f%%", 100 * best[k] / span))
					:wikitext(best[k]))
			end
			local oldMedianKey = {
				standing = 'stand',
				crouching = 'crouch',
			}
			local median = data.median[oldMedianKey[type]][punisher.enemy]
			if median then
				bars:node(mw.html.create('div')
					:addClass('punishment-median')
					:css('width', string.format("%.2f%%", 100 * median / span)))
			end
		end
		grid:node(mw.clone(scale))
	end
	
	return root
end

return p