No edit summary |
No edit summary |
||
Line 68: | Line 68: | ||
f(move) | f(move) | ||
end | |||
end | |||
local function fillDamage(damageCell, punisher) | |||
damageCell:wikitext(punisher.damage) | |||
if punisher.mini or punisher.staple or punisher.wall or punisher.rage then | |||
local character = assert(punisher.moveId:match('^(.-)%-'), 'moveId is invalid: ' .. punisher.moveId) | |||
local comboLinks = {} | |||
local insertComboLink = function(section, damage) | |||
if damage then | |||
table.insert(comboLinks, '[['..character..' combos#'..section..'|'..damage..']]') | |||
end | |||
end | |||
insertComboLink('Mini-combos', punisher.mini) | |||
insertComboLink('Staples', punisher.staple) | |||
insertComboLink('Wall', punisher.wall) | |||
insertComboLink('Rage', punisher.rage) | |||
damageCell:wikitext(' (' .. table.concat(comboLinks, '/') .. ')') | |||
end | end | ||
end | end | ||
Line 257: | Line 277: | ||
end | end | ||
fillDamage(row:tag('td'), punisher) | |||
row:tag('td'):wikitext(punisher.frames) | row:tag('td'):wikitext(punisher.frames) | ||
Line 553: | Line 556: | ||
row:tag('td'):wikitext(punisher.speed) | row:tag('td'):wikitext(punisher.speed) | ||
row:tag('td'):wikitext(punisher.range) | row:tag('td'):wikitext(punisher.range) | ||
row:tag('td') | fillDamage(row:tag('td'), punisher) | ||
row:tag('td'):wikitext(punisher.risk) | row:tag('td'):wikitext(punisher.risk) | ||
row:tag('td'):wikitext(punisher.hitbox) | row:tag('td'):wikitext(punisher.hitbox) |
Revision as of 18:50, 15 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')
return str == '1'
end
function boolToCargoBool(b)
assert(type(b) == 'boolean')
if b then
return '1'
else
return nil
end
end
function moveNotFoundMsg(id)
return "move with id = '" .. id .. "' not found"
end
--[[
Calls 'f' for every move in a string ending with 'moveId'.
Parameters:
moveId - id of the last move in a string.
columns - list of columns to select from MoveDataCargoTest table.
Should not contain 'parent', it is selected by default.
f - function that will be called for every move in a string, including last.
Takes 1 parameter: move - table containing key-value pairs (column name -> column value).
Throws:
- if there is a cycle in a string (2 moves reference each other and etc.).
--]]
local function forEachMove(moveId, columns, f)
local visitedParents = { sorted = {} }
while moveId and moveId ~= '' do
visitedParents[moveId] = true
table.insert(visitedParents.sorted, moveId)
local move = cargo.query(
tables,
'parent,'..table.concat(columns, ','),
{ where = "id='"..moveId.."'" }
)[1]
assert(move, moveNotFoundMsg(moveId))
moveId = move['parent']
assert(not visitedParents[moveId], 'Found parent cycle: ' .. table.concat(visitedParents.sorted, ', ') .. ', ' .. moveId)
f(move)
end
end
local function fillDamage(damageCell, punisher)
damageCell:wikitext(punisher.damage)
if punisher.mini or punisher.staple or punisher.wall or punisher.rage then
local character = assert(punisher.moveId:match('^(.-)%-'), 'moveId is invalid: ' .. punisher.moveId)
local comboLinks = {}
local insertComboLink = function(section, damage)
if damage then
table.insert(comboLinks, '[['..character..' combos#'..section..'|'..damage..']]')
end
end
insertComboLink('Mini-combos', punisher.mini)
insertComboLink('Staples', punisher.staple)
insertComboLink('Wall', punisher.wall)
insertComboLink('Rage', punisher.rage)
damageCell:wikitext(' (' .. table.concat(comboLinks, '/') .. ')')
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
p.display = function(frame)
local function appendFrom(dst, src)
for k, v in pairs(src) do
dst[k] = v
end
end
local leads = {}
forEachMove(frame:getParent().args['parent'], {'input','target','damage'}, function(parent)
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)
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 = nil
if k == 'reach' then
override = frame:getParent().args['range']
else
override = frame:getParent().args[k]
end
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..']]'
result['range'] = result['reach']
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
decoded.hard = decoded.hard == 'true'
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 startup = nil
local input = ''
local damage = 0
local hit = nil
local moveId = punisher.moveId
forEachMove(moveId, {'startup','input','damage','hit'}, function(move)
startup = 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
end)
if not punisher.enemy then
punisher.enemy = -getFirstStartupFrame(startup)
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 = maxOrNil(punisher.staple, punisher.mini),
wallCombo = punisher.wall,
rageCombo = punisher.rage,
hard = boolToCargoBool(punisher.hard),
} }
end
end
local punishersByType = {}
for type, punishers in pairs(punishersByTypeUnsorted) do
queryMoveInfo(punishers)
store(punishers, type, frame)
table.sort(punishers, function(l, r) return l.enemy > r.enemy end)
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]
rowspansByDisadvantage[punisher.enemy] = (count or 0) + 1
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
fillDamage(row:tag('td'), punisher)
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
span = math.max(span, dmg or 0)
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
p.whiffPunisherTable = function(frame)
local function notFoundMsg(i, name)
return 'required parameter punisher['..i..'].' .. name .. ' not found'
end
local punishers = {}
for i, v in ipairs(frame:getParent().args) do
local decoded = O.decode(v)
assert(decoded.moveId, notFoundMsg(i, 'moveId'))
table.insert(punishers, decoded)
end
return p._whiffPunisherTable(frame, punishers)
end
p._whiffPunisherTable = function(frame, punishers)
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 startup = nil
local range = ''
local input = ''
local damage = 0
local block = nil
local target = ''
local moveId = punisher.moveId
forEachMove(moveId, {'startup','reach','input','damage','block','target'}, function(move)
startup = move['startup']
range = move['reach']
input = move['input'] .. input
for k, v in string.gmatch(move['damage'], "(%d+)") do
damage = damage + tonumber(k)
end
if not block then
block = move['block']
end
target = move['target']
end)
if not punisher.speed then
punisher.speed = startup
end
punisher.range = range
if not punisher.input then
punisher.input = input
end
if not punisher.damage then
punisher.damage = damage
end
if not punisher.risk then
punisher.risk = block
end
punisher.hitbox = target
end
end
queryMoveInfo(punishers)
local rows = ''
for _, punisher in ipairs(punishers) do
local row = mw.html.create('tr')
row:tag('td'):wikitext(punisher.input)
row:tag('td'):wikitext(punisher.speed)
row:tag('td'):wikitext(punisher.range)
fillDamage(row:tag('td'), punisher)
row:tag('td'):wikitext(punisher.risk)
row:tag('td'):wikitext(punisher.hitbox)
rows = rows .. tostring(row)
end
return frame:expandTemplate{
title = 'MoveDataCargoTest/WhiffPunisherTable/Display',
args = { rows }
}
end
return p