No edit summary |
No edit summary |
||
Line 212: | Line 212: | ||
enemy = punisher.enemy, | enemy = punisher.enemy, | ||
damage = punisher.damage, | damage = punisher.damage, | ||
combo = punisher.staple, | combo = maxOrNil(punisher.staple, punisher.mini), | ||
wallCombo = punisher.wall, | wallCombo = punisher.wall, | ||
rageCombo = punisher.rage, | rageCombo = punisher.rage, |
Revision as of 20:50, 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
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
while moveId ~= '' do
local move = cargo.query(tables, 'parent,startup,input,damage,hit', {
where = "id = '" .. moveId .. "'"
})[1]
assert(move, moveNotFoundMsg(moveId))
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
moveId = move['parent']
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]
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.mini then
damageCell:tag('br')
damageCell:wikitext(punisher.mini .. ' with mini combo')
end
if punisher.staple then
damageCell:tag('br')
damageCell:wikitext(punisher.staple .. ' with combo')
end
if punisher.wall then
damageCell:tag('br')
damageCell:wikitext(punisher.wall .. ' with a wall')
end
if punisher.rage then
damageCell:tag('br')
damageCell:wikitext(punisher.rage .. ' 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