|
|
(16 intermediate revisions by 2 users not shown) |
Line 1: |
Line 1: |
| local O = require("Module:O") | | local p = require("Module:Move") |
| local cargo = mw.ext.cargo
| | p.game = require("Module:Game").Tekken7 |
| | |
| 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
| |
| | |
| local function appendFrom(dst, src)
| |
| for k, v in pairs(src) do
| |
| dst[k] = v
| |
| 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
| |
| | |
| local function moveNotFoundMsg(id)
| |
| return "move with id = '" .. id .. "' not found"
| |
| end
| |
| | |
| local function parseMoveId(id)
| |
| -- '#' cannot be used within cargo query
| |
| -- see: https://www.mediawiki.org/wiki/Extension_talk:Cargo/Archive_January_to_February_2020#Error_in_%22where%22_parameter:_the_string_%22#%22_cannot_be_used_within_#cargo_query.
| |
| if id:find('#') then
| |
| return id:gsub('#', '#')
| |
| else
| |
| return id
| |
| end
| |
| end
| |
| | |
| local function parseMove(args)
| |
| assert(args.id)
| |
|
| |
| local result = mw.clone(args)
| |
|
| |
| result.id = parseMoveId(result.id)
| |
|
| |
| if result.range then
| |
| result.reach = result.range
| |
| result.range = nil
| |
| end
| |
|
| |
| return result
| |
| end
| |
| | |
| local function storeMove(argsUnparsed, frame)
| |
| local args = {'_table=Move7'}
| |
| appendFrom(args, parseMove(argsUnparsed))
| |
|
| |
| if args.id:find('#') or args.id:find('#', 1, true) then
| |
| assert(false, args.id)
| |
| end
| |
| frame:callParserFunction{ name = '#cargo_store', args = args }
| |
| end
| |
| | |
| local function queryMove(id, args)
| |
| local id = parseMoveId(id)
| |
|
| |
| local result = cargo.query('Move7', args, { where = "id = '"..id.."'" })[1]
| |
| assert(result, moveNotFoundMsg(id))
| |
| return result
| |
| 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 = queryMove(moveId, 'parent,'..table.concat(columns, ','))
| |
| | |
| 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
| |
| damageCell:wikitext(punisher.damageNote)
| |
| end
| |
| | |
| local function fillDamageOnBackturned(damageCell, punisher)
| |
| damageCell:wikitext(punisher.damage)
| |
| if punisher.staple then
| |
| local character = assert(punisher.moveId:match('^(.-)%-'), 'moveId is invalid: ' .. punisher.moveId)
| |
| damageCell:wikitext(' ([['..character..' combos#Back-turned opponent|'..punisher.staple..']])')
| |
| end
| |
| damageCell:wikitext(punisher.damageNote)
| |
| end
| |
| | |
| | |
| local p = {};
| |
| | |
| p.store = function(frame) | |
| storeMove(frame:getParent().args, frame)
| |
| end
| |
| | |
| p.display = function(frame)
| |
| 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 = 'MoveDisplay7', args = frame:getParent().args }
| |
| end
| |
|
| |
| local args = {}
| |
| appendFrom(args, frame:getParent().args)
| |
| appendFrom(args, leads)
| |
| return frame:expandTemplate{ title = 'MoveDisplay7', args = args }
| |
| end
| |
| | |
| p.inherit = function(frame)
| |
| local 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
| |
|
| |
| local id = getQueryId(frame)
| |
| local result = queryMove(
| |
| id,
| |
| 'id,parent,name,input,target,damage,reach,tracksLeft,tracksRight,startup,recv,tot,crush,block,hit,ch,notes'
| |
| .. ',Move7._pageName=page'
| |
| )
| |
|
| |
| 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
| |
|
| |
| storeMove(result, frame)
| |
|
| |
| result['inheritedFrom'] = '[['..result['page']..'#'..id..'|'..id..']]'
| |
| result['range'] = result['reach']
| |
| return frame:expandTemplate{ title = 'MoveImpl7', 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.move then
| |
| punisher.move = 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=Punisher7',
| |
| 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 function isSorted(t, pred)
| |
| for i = 1, #t - 1, 1 do
| |
| if not pred(t[i], t[i + 1]) then
| |
| return false
| |
| end
| |
| end
| |
| return true
| |
| end
| |
| local punishersByType = {}
| |
| for type, punishers in pairs(punishersByTypeUnsorted) do
| |
| queryMoveInfo(punishers)
| |
| store(punishers, type, frame)
| |
|
| |
| if not isSorted(punishers, function(l, r) return l.enemy >= r.enemy end) then
| |
| table.sort(punishers, function(l, r) return l.enemy > r.enemy end)
| |
| 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'):attr('rowspan', rowspan)
| |
| :wikitext(punisher.enemy, punisher.enemyNote)
| |
| rowspansByDisadvantage[punisher.enemy] = nil
| |
| end
| |
|
| |
| local moveCell = row:tag('td'):wikitext(punisher.move)
| |
| if punisher.hard then
| |
| moveCell:tag('sup'):wikitext('[hard]')
| |
| end
| |
| moveCell:wikitext(punisher.moveNote)
| |
|
| |
| if punishers.type == 'backTurnedOpponent' then
| |
| fillDamageOnBackturned(row:tag('td'), punisher)
| |
| else
| |
| fillDamage(row:tag('td'), punisher)
| |
| end
| |
|
| |
| row:tag('td'):wikitext(punisher.frames, punisher.framesNote)
| |
|
| |
| rows = rows .. tostring(row)
| |
| end
| |
|
| |
| displayArgs[punishers.type] = rows
| |
| end
| |
|
| |
| return frame:expandTemplate{
| |
| title = 'PunisherTableDisplay7',
| |
| args = displayArgs
| |
| }
| |
| end
| |
| | |
| p.punishmentChart = 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(
| |
| 'Punisher7',
| |
| 'type,enemy,damage,combo,wallCombo,rageCombo,hard',
| |
| { where =
| |
| "Punisher7._pageName='"..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
| |
|
| |
| -- Estimating this for now
| |
| local median = {
| |
| standing = {
| |
| [10] = 24,
| |
| [11] = 27,
| |
| [12] = 30,
| |
| [13] = 33,
| |
| [14] = 36,
| |
| [15] = 65,
| |
| [23] = 77,
| |
| },
| |
| crouching = {
| |
| [10] = 5,
| |
| [11] = 20,
| |
| [12] = 25,
| |
| [13] = 30,
| |
| [14] = 35,
| |
| [15] = 65,
| |
| [23] = 77,
| |
| },
| |
| }
| |
| 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 median = median[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.move then
| |
| punisher.move = input
| |
| end
| |
| if not punisher.speed then
| |
| punisher.speed = startup
| |
| end
| |
| if not punisher.range then
| |
| punisher.range = range
| |
| end
| |
| if not punisher.damage then
| |
| punisher.damage = damage
| |
| end
| |
| if not punisher.risk then
| |
| punisher.risk = block
| |
| end
| |
| if not punisher.hitbox then
| |
| local t = assert(target:match('^([hHmMlLsS])'), 'invalid target: ' .. target)
| |
| :lower()
| |
| if target == 'h' then
| |
| punisher.hitbox = 'High'
| |
| elseif target == 'm' or target == 's' then
| |
| punisher.hitbox = 'Mid'
| |
| elseif target == 'l' then
| |
| punisher.hitbox = 'Low'
| |
| end
| |
| end
| |
| end
| |
| end
| |
|
| |
| queryMoveInfo(punishers)
| |
|
| |
| local rows = ''
| |
| for _, punisher in ipairs(punishers) do
| |
| local row = mw.html.create('tr')
| |
| row:tag('td'):wikitext(punisher.move, punisher.moveNote)
| |
| row:tag('td'):wikitext(punisher.speed, punisher.speedNote)
| |
| row:tag('td'):wikitext(punisher.range, punisher.rangeNote)
| |
| fillDamage(row:tag('td'), punisher)
| |
| row:tag('td'):wikitext(punisher.risk, punisher.riskNote)
| |
| row:tag('td'):wikitext(punisher.hitbox, punisher.hitboxNote)
| |
|
| |
| rows = rows .. tostring(row)
| |
| end
| |
| return frame:expandTemplate{
| |
| title = 'WhiffPunisherTableDisplay7',
| |
| args = { rows }
| |
| }
| |
| end
| |
| | |
| return p | | return p |