UNPKG

@slippi/slippi-js

Version:
1,450 lines (1,432 loc) 165 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var get = require('lodash/get'); var isEqual = require('lodash/isEqual'); var keyBy = require('lodash/keyBy'); var last = require('lodash/last'); var set = require('lodash/set'); var size = require('lodash/size'); var events = require('events'); var filter = require('lodash/filter'); var groupBy = require('lodash/groupBy'); var orderBy = require('lodash/orderBy'); var first = require('lodash/first'); var flatten = require('lodash/flatten'); var mapValues = require('lodash/mapValues'); var zip = require('lodash/zip'); var dateFns = require('date-fns'); var fs = require('fs'); var forEach = require('lodash/forEach'); var stream = require('stream'); var ubjson = require('@shelacek/ubjson'); var net = require('net'); var inject = require('reconnect-core'); var iconv = require('iconv-lite'); var map = require('lodash/map'); var path = require('path'); var semver = require('semver'); // eslint-disable-next-line function getDeathDirection(actionStateId) { if (actionStateId > 0xa) { return null; } switch (actionStateId) { case 0: return "down"; case 1: return "left"; case 2: return "right"; default: return "up"; } } var animationUtils = { __proto__: null, getDeathDirection: getDeathDirection }; var characters = { "0": { name: "Captain Falcon", shortName: "Falcon", colors: [ "Black", "Red", "White", "Green", "Blue" ] }, "1": { name: "Donkey Kong", shortName: "DK", colors: [ "Black", "Red", "Blue", "Green" ] }, "2": { name: "Fox", colors: [ "Red", "Blue", "Green" ] }, "3": { name: "Mr. Game & Watch", shortName: "G&W", colors: [ "Red", "Blue", "Green" ] }, "4": { name: "Kirby", colors: [ "Yellow", "Blue", "Red", "Green", "White" ] }, "5": { name: "Bowser", colors: [ "Red", "Blue", "Black" ] }, "6": { name: "Link", colors: [ "Red", "Blue", "Black", "White" ] }, "7": { name: "Luigi", colors: [ "White", "Blue", "Red" ] }, "8": { name: "Mario", colors: [ "Yellow", "Black", "Blue", "Green" ] }, "9": { name: "Marth", colors: [ "Red", "Green", "Black", "White" ] }, "10": { name: "Mewtwo", colors: [ "Red", "Blue", "Green" ] }, "11": { name: "Ness", colors: [ "Yellow", "Blue", "Green" ] }, "12": { name: "Peach", colors: [ "Daisy", "White", "Blue", "Green" ] }, "13": { name: "Pikachu", colors: [ "Red", "Party Hat", "Cowboy Hat" ] }, "14": { name: "Ice Climbers", shortName: "ICs", colors: [ "Green", "Orange", "Red" ] }, "15": { name: "Jigglypuff", shortName: "Puff", colors: [ "Red", "Blue", "Headband", "Crown" ] }, "16": { name: "Samus", colors: [ "Pink", "Black", "Green", "Purple" ] }, "17": { name: "Yoshi", colors: [ "Red", "Blue", "Yellow", "Pink", "Cyan" ] }, "18": { name: "Zelda", colors: [ "Red", "Blue", "Green", "White" ] }, "19": { name: "Sheik", colors: [ "Red", "Blue", "Green", "White" ] }, "20": { name: "Falco", colors: [ "Red", "Blue", "Green" ] }, "21": { name: "Young Link", shortName: "YLink", colors: [ "Red", "Blue", "White", "Black" ] }, "22": { name: "Dr. Mario", shortName: "Doc", colors: [ "Red", "Blue", "Green", "Black" ] }, "23": { name: "Roy", colors: [ "Red", "Blue", "Green", "Yellow" ] }, "24": { name: "Pichu", colors: [ "Red", "Blue", "Green" ] }, "25": { name: "Ganondorf", shortName: "Ganon", colors: [ "Red", "Blue", "Green", "Purple" ] }, "26": { name: "Master Hand" }, "27": { name: "Wireframe (Male)" }, "28": { name: "Wireframe (Female)" }, "29": { name: "Gigabowser" }, "30": { name: "Crazy Hand" }, "31": { name: "Sandbag" }, "32": { name: "Popo" } }; const DEFAULT_COLOR = "Default"; const UnknownCharacter = { id: -1, name: "Unknown Character", shortName: "Unknown", colors: [DEFAULT_COLOR] }; function generateCharacterInfo(id, info) { var _info$shortName, _info$colors; if (!info) { return UnknownCharacter; } return { id, name: info.name, shortName: (_info$shortName = info.shortName) != null ? _info$shortName : info.name, colors: [DEFAULT_COLOR, ...((_info$colors = info.colors) != null ? _info$colors : [])] }; } function getAllCharacters() { return Object.entries(characters).map(([id, data]) => generateCharacterInfo(parseInt(id, 10), data)).sort((a, b) => a.id - b.id); } function getCharacterInfo(externalCharacterId) { const data = characters[externalCharacterId.toString()]; return generateCharacterInfo(externalCharacterId, data); } function getCharacterShortName(externalCharacterId) { const character = getCharacterInfo(externalCharacterId); return character.shortName; } function getCharacterName(externalCharacterId) { const character = getCharacterInfo(externalCharacterId); return character.name; } // Return a human-readable color from a characterCode. function getCharacterColorName(externalCharacterId, characterColor) { const character = getCharacterInfo(externalCharacterId); const color = character.colors[characterColor]; if (color) { return color; } return DEFAULT_COLOR; } var characterUtils = { __proto__: null, UnknownCharacter: UnknownCharacter, getAllCharacters: getAllCharacters, getCharacterColorName: getCharacterColorName, getCharacterInfo: getCharacterInfo, getCharacterName: getCharacterName, getCharacterShortName: getCharacterShortName }; var moveNames = { "1": { name: "Miscellaneous", shortName: "misc" }, "2": { name: "Jab", shortName: "jab" }, "3": { name: "Jab", shortName: "jab" }, "4": { name: "Jab", shortName: "jab" }, "5": { name: "Rapid Jabs", shortName: "rapid-jabs" }, "6": { name: "Dash Attack", shortName: "dash" }, "7": { name: "Forward Tilt", shortName: "ftilt" }, "8": { name: "Up Tilt", shortName: "utilt" }, "9": { name: "Down Tilt", shortName: "dtilt" }, "10": { name: "Forward Smash", shortName: "fsmash" }, "11": { name: "Up Smash", shortName: "usmash" }, "12": { name: "Down Smash", shortName: "dsmash" }, "13": { name: "Neutral Air", shortName: "nair" }, "14": { name: "Forward Air", shortName: "fair" }, "15": { name: "Back Air", shortName: "bair" }, "16": { name: "Up Air", shortName: "uair" }, "17": { name: "Down Air", shortName: "dair" }, "18": { name: "Neutral B", shortName: "neutral-b" }, "19": { name: "Side B", shortName: "side-b" }, "20": { name: "Up B", shortName: "up-b" }, "21": { name: "Down B", shortName: "down-b" }, "50": { name: "Getup Attack", shortName: "getup" }, "51": { name: "Getup Attack (Slow)", shortName: "getup-slow" }, "52": { name: "Grab Pummel", shortName: "pummel" }, "53": { name: "Forward Throw", shortName: "fthrow" }, "54": { name: "Back Throw", shortName: "bthrow" }, "55": { name: "Up Throw", shortName: "uthrow" }, "56": { name: "Down Throw", shortName: "dthrow" }, "61": { name: "Edge Attack (Slow)", shortName: "edge-slow" }, "62": { name: "Edge Attack", shortName: "edge" } }; const UnknownMove = { id: -1, name: "Unknown Move", shortName: "unknown" }; function getMoveInfo(moveId) { const moveName = moveNames[moveId.toString()]; if (!moveName) { return UnknownMove; } return { id: moveId, name: moveName.name, shortName: moveName.shortName }; } function getMoveShortName(moveId) { const move = getMoveInfo(moveId); return move.shortName; } function getMoveName(moveId) { const move = getMoveInfo(moveId); return move.name; } var moveUtils = { __proto__: null, UnknownMove: UnknownMove, getMoveInfo: getMoveInfo, getMoveName: getMoveName, getMoveShortName: getMoveShortName }; var stageNames = { "2": "Fountain of Dreams", "3": "Pokémon Stadium", "4": "Princess Peach's Castle", "5": "Kongo Jungle", "6": "Brinstar", "7": "Corneria", "8": "Yoshi's Story", "9": "Onett", "10": "Mute City", "11": "Rainbow Cruise", "12": "Jungle Japes", "13": "Great Bay", "14": "Hyrule Temple", "15": "Brinstar Depths", "16": "Yoshi's Island", "17": "Green Greens", "18": "Fourside", "19": "Mushroom Kingdom I", "20": "Mushroom Kingdom II", "22": "Venom", "23": "Poké Floats", "24": "Big Blue", "25": "Icicle Mountain", "26": "Icetop", "27": "Flat Zone", "28": "Dream Land N64", "29": "Yoshi's Island N64", "30": "Kongo Jungle N64", "31": "Battlefield", "32": "Final Destination", "33": "Target Test (Mario)", "34": "Target Test (Captain Falcon)", "35": "Target Test (Young Link)", "36": "Target Test (Donkey Kong)", "37": "Target Test (Dr. Mario)", "38": "Target Test (Falco)", "39": "Target Test (Fox)", "40": "Target Test (Ice Climbers)", "41": "Target Test (Kirby)", "42": "Target Test (Bowser)", "43": "Target Test (Link)", "44": "Target Test (Luigi)", "45": "Target Test (Marth)", "46": "Target Test (Mewtwo)", "47": "Target Test (Ness)", "48": "Target Test (Peach)", "49": "Target Test (Pichu)", "50": "Target Test (Pikachu)", "51": "Target Test (Jigglypuff)", "52": "Target Test (Samus)", "53": "Target Test (Sheik)", "54": "Target Test (Yoshi)", "55": "Target Test (Zelda)", "56": "Target Test (Mr. Game & Watch)", "57": "Target Test (Roy)", "58": "Target Test (Ganondorf)", "84": "Home-Run Contest" }; const UnknownStage = { id: -1, name: "Unknown Stage" }; function getStageInfo(stageId) { const stageName = stageNames[stageId.toString()]; if (!stageName) { return UnknownStage; } return { id: stageId, name: stageName }; } function getStageName(stageId) { const stage = getStageInfo(stageId); return stage.name; } var stageUtils = { __proto__: null, UnknownStage: UnknownStage, getStageInfo: getStageInfo, getStageName: getStageName }; exports.Character = void 0; (function (Character) { Character[Character["CAPTAIN_FALCON"] = 0] = "CAPTAIN_FALCON"; Character[Character["DONKEY_KONG"] = 1] = "DONKEY_KONG"; Character[Character["FOX"] = 2] = "FOX"; Character[Character["GAME_AND_WATCH"] = 3] = "GAME_AND_WATCH"; Character[Character["KIRBY"] = 4] = "KIRBY"; Character[Character["BOWSER"] = 5] = "BOWSER"; Character[Character["LINK"] = 6] = "LINK"; Character[Character["LUIGI"] = 7] = "LUIGI"; Character[Character["MARIO"] = 8] = "MARIO"; Character[Character["MARTH"] = 9] = "MARTH"; Character[Character["MEWTWO"] = 10] = "MEWTWO"; Character[Character["NESS"] = 11] = "NESS"; Character[Character["PEACH"] = 12] = "PEACH"; Character[Character["PIKACHU"] = 13] = "PIKACHU"; Character[Character["ICE_CLIMBERS"] = 14] = "ICE_CLIMBERS"; Character[Character["JIGGLYPUFF"] = 15] = "JIGGLYPUFF"; Character[Character["SAMUS"] = 16] = "SAMUS"; Character[Character["YOSHI"] = 17] = "YOSHI"; Character[Character["ZELDA"] = 18] = "ZELDA"; Character[Character["SHEIK"] = 19] = "SHEIK"; Character[Character["FALCO"] = 20] = "FALCO"; Character[Character["YOUNG_LINK"] = 21] = "YOUNG_LINK"; Character[Character["DR_MARIO"] = 22] = "DR_MARIO"; Character[Character["ROY"] = 23] = "ROY"; Character[Character["PICHU"] = 24] = "PICHU"; Character[Character["GANONDORF"] = 25] = "GANONDORF"; Character[Character["MASTER_HAND"] = 26] = "MASTER_HAND"; Character[Character["WIREFRAME_MALE"] = 27] = "WIREFRAME_MALE"; Character[Character["WIREFRAME_FEMALE"] = 28] = "WIREFRAME_FEMALE"; Character[Character["GIGA_BOWSER"] = 29] = "GIGA_BOWSER"; Character[Character["CRAZY_HAND"] = 30] = "CRAZY_HAND"; Character[Character["SANDBAG"] = 31] = "SANDBAG"; Character[Character["POPO"] = 32] = "POPO"; })(exports.Character || (exports.Character = {})); exports.Stage = void 0; (function (Stage) { Stage[Stage["FOUNTAIN_OF_DREAMS"] = 2] = "FOUNTAIN_OF_DREAMS"; Stage[Stage["POKEMON_STADIUM"] = 3] = "POKEMON_STADIUM"; Stage[Stage["PEACHS_CASTLE"] = 4] = "PEACHS_CASTLE"; Stage[Stage["KONGO_JUNGLE"] = 5] = "KONGO_JUNGLE"; Stage[Stage["BRINSTAR"] = 6] = "BRINSTAR"; Stage[Stage["CORNERIA"] = 7] = "CORNERIA"; Stage[Stage["YOSHIS_STORY"] = 8] = "YOSHIS_STORY"; Stage[Stage["ONETT"] = 9] = "ONETT"; Stage[Stage["MUTE_CITY"] = 10] = "MUTE_CITY"; Stage[Stage["RAINBOW_CRUISE"] = 11] = "RAINBOW_CRUISE"; Stage[Stage["JUNGLE_JAPES"] = 12] = "JUNGLE_JAPES"; Stage[Stage["GREAT_BAY"] = 13] = "GREAT_BAY"; Stage[Stage["HYRULE_TEMPLE"] = 14] = "HYRULE_TEMPLE"; Stage[Stage["BRINSTAR_DEPTHS"] = 15] = "BRINSTAR_DEPTHS"; Stage[Stage["YOSHIS_ISLAND"] = 16] = "YOSHIS_ISLAND"; Stage[Stage["GREEN_GREENS"] = 17] = "GREEN_GREENS"; Stage[Stage["FOURSIDE"] = 18] = "FOURSIDE"; Stage[Stage["MUSHROOM_KINGDOM"] = 19] = "MUSHROOM_KINGDOM"; Stage[Stage["MUSHROOM_KINGDOM_2"] = 20] = "MUSHROOM_KINGDOM_2"; Stage[Stage["VENOM"] = 22] = "VENOM"; Stage[Stage["POKE_FLOATS"] = 23] = "POKE_FLOATS"; Stage[Stage["BIG_BLUE"] = 24] = "BIG_BLUE"; Stage[Stage["ICICLE_MOUNTAIN"] = 25] = "ICICLE_MOUNTAIN"; Stage[Stage["ICETOP"] = 26] = "ICETOP"; Stage[Stage["FLAT_ZONE"] = 27] = "FLAT_ZONE"; Stage[Stage["DREAMLAND"] = 28] = "DREAMLAND"; Stage[Stage["YOSHIS_ISLAND_N64"] = 29] = "YOSHIS_ISLAND_N64"; Stage[Stage["KONGO_JUNGLE_N64"] = 30] = "KONGO_JUNGLE_N64"; Stage[Stage["BATTLEFIELD"] = 31] = "BATTLEFIELD"; Stage[Stage["FINAL_DESTINATION"] = 32] = "FINAL_DESTINATION"; Stage[Stage["TARGET_TEST_MARIO"] = 33] = "TARGET_TEST_MARIO"; Stage[Stage["TARGET_TEST_CAPTAIN_FALCON"] = 34] = "TARGET_TEST_CAPTAIN_FALCON"; Stage[Stage["TARGET_TEST_YOUNG_LINK"] = 35] = "TARGET_TEST_YOUNG_LINK"; Stage[Stage["TARGET_TEST_DONKEY_KONG"] = 36] = "TARGET_TEST_DONKEY_KONG"; Stage[Stage["TARGET_TEST_DR_MARIO"] = 37] = "TARGET_TEST_DR_MARIO"; Stage[Stage["TARGET_TEST_FALCO"] = 38] = "TARGET_TEST_FALCO"; Stage[Stage["TARGET_TEST_FOX"] = 39] = "TARGET_TEST_FOX"; Stage[Stage["TARGET_TEST_ICE_CLIMBERS"] = 40] = "TARGET_TEST_ICE_CLIMBERS"; Stage[Stage["TARGET_TEST_KIRBY"] = 41] = "TARGET_TEST_KIRBY"; Stage[Stage["TARGET_TEST_BOWSER"] = 42] = "TARGET_TEST_BOWSER"; Stage[Stage["TARGET_TEST_LINK"] = 43] = "TARGET_TEST_LINK"; Stage[Stage["TARGET_TEST_LUIGI"] = 44] = "TARGET_TEST_LUIGI"; Stage[Stage["TARGET_TEST_MARTH"] = 45] = "TARGET_TEST_MARTH"; Stage[Stage["TARGET_TEST_MEWTWO"] = 46] = "TARGET_TEST_MEWTWO"; Stage[Stage["TARGET_TEST_NESS"] = 47] = "TARGET_TEST_NESS"; Stage[Stage["TARGET_TEST_PEACH"] = 48] = "TARGET_TEST_PEACH"; Stage[Stage["TARGET_TEST_PICHU"] = 49] = "TARGET_TEST_PICHU"; Stage[Stage["TARGET_TEST_PIKACHU"] = 50] = "TARGET_TEST_PIKACHU"; Stage[Stage["TARGET_TEST_JIGGLYPUFF"] = 51] = "TARGET_TEST_JIGGLYPUFF"; Stage[Stage["TARGET_TEST_SAMUS"] = 52] = "TARGET_TEST_SAMUS"; Stage[Stage["TARGET_TEST_SHEIK"] = 53] = "TARGET_TEST_SHEIK"; Stage[Stage["TARGET_TEST_YOSHI"] = 54] = "TARGET_TEST_YOSHI"; Stage[Stage["TARGET_TEST_ZELDA"] = 55] = "TARGET_TEST_ZELDA"; Stage[Stage["TARGET_TEST_GAME_AND_WATCH"] = 56] = "TARGET_TEST_GAME_AND_WATCH"; Stage[Stage["TARGET_TEST_ROY"] = 57] = "TARGET_TEST_ROY"; Stage[Stage["TARGET_TEST_GANONDORF"] = 58] = "TARGET_TEST_GANONDORF"; Stage[Stage["RACE_TO_THE_FINISH"] = 82] = "RACE_TO_THE_FINISH"; Stage[Stage["GRAB_THE_TROPHIES"] = 83] = "GRAB_THE_TROPHIES"; Stage[Stage["HOME_RUN_CONTEST"] = 84] = "HOME_RUN_CONTEST"; Stage[Stage["ALL_STAR_LOBBY"] = 85] = "ALL_STAR_LOBBY"; Stage[Stage["EVENT_ONE"] = 202] = "EVENT_ONE"; Stage[Stage["EVENT_EIGHTEEN"] = 203] = "EVENT_EIGHTEEN"; Stage[Stage["EVENT_THREE"] = 204] = "EVENT_THREE"; Stage[Stage["EVENT_FOUR"] = 205] = "EVENT_FOUR"; Stage[Stage["EVENT_FIVE"] = 206] = "EVENT_FIVE"; Stage[Stage["EVENT_SIX"] = 207] = "EVENT_SIX"; Stage[Stage["EVENT_SEVEN"] = 208] = "EVENT_SEVEN"; Stage[Stage["EVENT_EIGHT"] = 209] = "EVENT_EIGHT"; Stage[Stage["EVENT_NINE"] = 210] = "EVENT_NINE"; Stage[Stage["EVENT_TEN_PART_ONE"] = 211] = "EVENT_TEN_PART_ONE"; Stage[Stage["EVENT_ELEVEN"] = 212] = "EVENT_ELEVEN"; Stage[Stage["EVENT_TWELVE"] = 213] = "EVENT_TWELVE"; Stage[Stage["EVENT_THIRTEEN"] = 214] = "EVENT_THIRTEEN"; Stage[Stage["EVENT_FOURTEEN"] = 215] = "EVENT_FOURTEEN"; Stage[Stage["EVENT_THIRTY_SEVEN"] = 216] = "EVENT_THIRTY_SEVEN"; Stage[Stage["EVENT_SIXTEEN"] = 217] = "EVENT_SIXTEEN"; Stage[Stage["EVENT_SEVENTEEN"] = 218] = "EVENT_SEVENTEEN"; Stage[Stage["EVENT_TWO"] = 219] = "EVENT_TWO"; Stage[Stage["EVENT_NINETEEN"] = 220] = "EVENT_NINETEEN"; Stage[Stage["EVENT_TWENTY_PART_ONE"] = 221] = "EVENT_TWENTY_PART_ONE"; Stage[Stage["EVENT_TWENTY_ONE"] = 222] = "EVENT_TWENTY_ONE"; Stage[Stage["EVENT_TWENTY_TWO"] = 223] = "EVENT_TWENTY_TWO"; Stage[Stage["EVENT_TWENTY_SEVEN"] = 224] = "EVENT_TWENTY_SEVEN"; Stage[Stage["EVENT_TWENTY_FOUR"] = 225] = "EVENT_TWENTY_FOUR"; Stage[Stage["EVENT_TWENTY_FIVE"] = 226] = "EVENT_TWENTY_FIVE"; Stage[Stage["EVENT_TWENTY_SIX"] = 227] = "EVENT_TWENTY_SIX"; Stage[Stage["EVENT_TWENTY_THREE"] = 228] = "EVENT_TWENTY_THREE"; Stage[Stage["EVENT_TWENTY_EIGHT"] = 229] = "EVENT_TWENTY_EIGHT"; Stage[Stage["EVENT_TWENTY_NINE"] = 230] = "EVENT_TWENTY_NINE"; Stage[Stage["EVENT_THIRTY_PART_ONE"] = 231] = "EVENT_THIRTY_PART_ONE"; Stage[Stage["EVENT_THIRTY_ONE"] = 232] = "EVENT_THIRTY_ONE"; Stage[Stage["EVENT_THIRTY_TWO"] = 233] = "EVENT_THIRTY_TWO"; Stage[Stage["EVENT_THIRTY_THREE"] = 234] = "EVENT_THIRTY_THREE"; Stage[Stage["EVENT_THIRTY_FOUR"] = 235] = "EVENT_THIRTY_FOUR"; Stage[Stage["EVENT_FORTY_EIGHT"] = 236] = "EVENT_FORTY_EIGHT"; Stage[Stage["EVENT_THIRTY_SIX_PART_ONE"] = 237] = "EVENT_THIRTY_SIX_PART_ONE"; Stage[Stage["EVENT_FIFTEEN"] = 238] = "EVENT_FIFTEEN"; Stage[Stage["EVENT_THIRTY_EIGHT"] = 239] = "EVENT_THIRTY_EIGHT"; Stage[Stage["EVENT_THIRTY_NINE"] = 240] = "EVENT_THIRTY_NINE"; Stage[Stage["EVENT_FORTY_PART_ONE"] = 241] = "EVENT_FORTY_PART_ONE"; Stage[Stage["EVENT_FORTY_ONE"] = 242] = "EVENT_FORTY_ONE"; Stage[Stage["EVENT_FORTY_TWO"] = 243] = "EVENT_FORTY_TWO"; Stage[Stage["EVENT_FORTY_THREE"] = 244] = "EVENT_FORTY_THREE"; Stage[Stage["EVENT_FORTY_FOUR"] = 245] = "EVENT_FORTY_FOUR"; Stage[Stage["EVENT_FORTY_FIVE"] = 246] = "EVENT_FORTY_FIVE"; Stage[Stage["EVENT_FORTY_SIX"] = 247] = "EVENT_FORTY_SIX"; Stage[Stage["EVENT_FORTY_SEVEN"] = 248] = "EVENT_FORTY_SEVEN"; Stage[Stage["EVENT_THIRTY_FIVE"] = 249] = "EVENT_THIRTY_FIVE"; Stage[Stage["EVENT_FORTY_NINE_PART_ONE"] = 250] = "EVENT_FORTY_NINE_PART_ONE"; Stage[Stage["EVENT_FIFTY"] = 251] = "EVENT_FIFTY"; Stage[Stage["EVENT_FIFTY_ONE"] = 252] = "EVENT_FIFTY_ONE"; Stage[Stage["EVENT_TEN_PART_TWO"] = 253] = "EVENT_TEN_PART_TWO"; Stage[Stage["EVENT_TEN_PART_THREE"] = 254] = "EVENT_TEN_PART_THREE"; Stage[Stage["EVENT_TEN_PART_FOUR"] = 255] = "EVENT_TEN_PART_FOUR"; Stage[Stage["EVENT_TEN_PART_FIVE"] = 256] = "EVENT_TEN_PART_FIVE"; Stage[Stage["EVENT_TWENTY_PART_TWO"] = 257] = "EVENT_TWENTY_PART_TWO"; Stage[Stage["EVENT_TWENTY_PART_THREE"] = 258] = "EVENT_TWENTY_PART_THREE"; Stage[Stage["EVENT_TWENTY_PART_FOUR"] = 259] = "EVENT_TWENTY_PART_FOUR"; Stage[Stage["EVENT_TWENTY_PART_FIVE"] = 260] = "EVENT_TWENTY_PART_FIVE"; Stage[Stage["EVENT_THIRTY_PART_TWO"] = 261] = "EVENT_THIRTY_PART_TWO"; Stage[Stage["EVENT_THIRTY_PART_THREE"] = 262] = "EVENT_THIRTY_PART_THREE"; Stage[Stage["EVENT_THIRTY_PART_FOUR"] = 263] = "EVENT_THIRTY_PART_FOUR"; Stage[Stage["EVENT_FORTY_PART_TWO"] = 264] = "EVENT_FORTY_PART_TWO"; Stage[Stage["EVENT_FORTY_PART_THREE"] = 265] = "EVENT_FORTY_PART_THREE"; Stage[Stage["EVENT_FORTY_PART_FOUR"] = 266] = "EVENT_FORTY_PART_FOUR"; Stage[Stage["EVENT_FORTY_PART_FIVE"] = 267] = "EVENT_FORTY_PART_FIVE"; Stage[Stage["EVENT_FORTY_NINE_PART_TWO"] = 268] = "EVENT_FORTY_NINE_PART_TWO"; Stage[Stage["EVENT_FORTY_NINE_PART_THREE"] = 269] = "EVENT_FORTY_NINE_PART_THREE"; Stage[Stage["EVENT_FORTY_NINE_PART_FOUR"] = 270] = "EVENT_FORTY_NINE_PART_FOUR"; Stage[Stage["EVENT_FORTY_NINE_PART_FIVE"] = 271] = "EVENT_FORTY_NINE_PART_FIVE"; Stage[Stage["EVENT_FORTY_NINE_PART_SIX"] = 272] = "EVENT_FORTY_NINE_PART_SIX"; Stage[Stage["EVENT_THIRTY_SIX_PART_TWO"] = 273] = "EVENT_THIRTY_SIX_PART_TWO"; Stage[Stage["MULTI_MAN_MELEE"] = 285] = "MULTI_MAN_MELEE"; })(exports.Stage || (exports.Stage = {})); exports.State = void 0; (function (State) { // Animation ID ranges State[State["DAMAGE_START"] = 75] = "DAMAGE_START"; State[State["DAMAGE_END"] = 91] = "DAMAGE_END"; State[State["CAPTURE_START"] = 223] = "CAPTURE_START"; State[State["CAPTURE_END"] = 232] = "CAPTURE_END"; State[State["GUARD_START"] = 178] = "GUARD_START"; State[State["GUARD_END"] = 182] = "GUARD_END"; State[State["GROUNDED_CONTROL_START"] = 14] = "GROUNDED_CONTROL_START"; State[State["GROUNDED_CONTROL_END"] = 24] = "GROUNDED_CONTROL_END"; State[State["SQUAT_START"] = 39] = "SQUAT_START"; State[State["SQUAT_END"] = 41] = "SQUAT_END"; State[State["DOWN_START"] = 183] = "DOWN_START"; State[State["DOWN_END"] = 198] = "DOWN_END"; State[State["TECH_START"] = 199] = "TECH_START"; State[State["TECH_END"] = 204] = "TECH_END"; State[State["DYING_START"] = 0] = "DYING_START"; State[State["DYING_END"] = 10] = "DYING_END"; State[State["CONTROLLED_JUMP_START"] = 24] = "CONTROLLED_JUMP_START"; State[State["CONTROLLED_JUMP_END"] = 34] = "CONTROLLED_JUMP_END"; State[State["GROUND_ATTACK_START"] = 44] = "GROUND_ATTACK_START"; State[State["GROUND_ATTACK_END"] = 64] = "GROUND_ATTACK_END"; State[State["AERIAL_ATTACK_START"] = 65] = "AERIAL_ATTACK_START"; State[State["AERIAL_ATTACK_END"] = 74] = "AERIAL_ATTACK_END"; State[State["ATTACK_FTILT_START"] = 51] = "ATTACK_FTILT_START"; State[State["ATTACK_FTILT_END"] = 55] = "ATTACK_FTILT_END"; State[State["ATTACK_FSMASH_START"] = 58] = "ATTACK_FSMASH_START"; State[State["ATTACK_FSMASH_END"] = 62] = "ATTACK_FSMASH_END"; // Animation ID specific State[State["ROLL_FORWARD"] = 233] = "ROLL_FORWARD"; State[State["ROLL_BACKWARD"] = 234] = "ROLL_BACKWARD"; State[State["SPOT_DODGE"] = 235] = "SPOT_DODGE"; State[State["AIR_DODGE"] = 236] = "AIR_DODGE"; State[State["ACTION_WAIT"] = 14] = "ACTION_WAIT"; State[State["ACTION_DASH"] = 20] = "ACTION_DASH"; State[State["ACTION_KNEE_BEND"] = 24] = "ACTION_KNEE_BEND"; State[State["GUARD_ON"] = 178] = "GUARD_ON"; State[State["TECH_MISS_UP"] = 183] = "TECH_MISS_UP"; State[State["JAB_RESET_UP"] = 185] = "JAB_RESET_UP"; State[State["TECH_MISS_DOWN"] = 191] = "TECH_MISS_DOWN"; State[State["JAB_RESET_DOWN"] = 193] = "JAB_RESET_DOWN"; State[State["NEUTRAL_TECH"] = 199] = "NEUTRAL_TECH"; State[State["FORWARD_TECH"] = 200] = "FORWARD_TECH"; State[State["BACKWARD_TECH"] = 201] = "BACKWARD_TECH"; State[State["WALL_TECH"] = 202] = "WALL_TECH"; State[State["MISSED_WALL_TECH"] = 247] = "MISSED_WALL_TECH"; State[State["DASH"] = 20] = "DASH"; State[State["TURN"] = 18] = "TURN"; State[State["LANDING_FALL_SPECIAL"] = 43] = "LANDING_FALL_SPECIAL"; State[State["JUMP_FORWARD"] = 25] = "JUMP_FORWARD"; State[State["JUMP_BACKWARD"] = 26] = "JUMP_BACKWARD"; State[State["FALL_FORWARD"] = 30] = "FALL_FORWARD"; State[State["FALL_BACKWARD"] = 31] = "FALL_BACKWARD"; State[State["GRAB"] = 212] = "GRAB"; State[State["DASH_GRAB"] = 214] = "DASH_GRAB"; State[State["GRAB_WAIT"] = 216] = "GRAB_WAIT"; State[State["PUMMEL"] = 217] = "PUMMEL"; State[State["CLIFF_CATCH"] = 252] = "CLIFF_CATCH"; State[State["THROW_UP"] = 221] = "THROW_UP"; State[State["THROW_FORWARD"] = 219] = "THROW_FORWARD"; State[State["THROW_DOWN"] = 222] = "THROW_DOWN"; State[State["THROW_BACK"] = 220] = "THROW_BACK"; State[State["DAMAGE_FALL"] = 38] = "DAMAGE_FALL"; State[State["ATTACK_JAB1"] = 44] = "ATTACK_JAB1"; State[State["ATTACK_JAB2"] = 45] = "ATTACK_JAB2"; State[State["ATTACK_JAB3"] = 46] = "ATTACK_JAB3"; State[State["ATTACK_JABM"] = 47] = "ATTACK_JABM"; State[State["ATTACK_DASH"] = 50] = "ATTACK_DASH"; State[State["ATTACK_UTILT"] = 56] = "ATTACK_UTILT"; State[State["ATTACK_DTILT"] = 57] = "ATTACK_DTILT"; State[State["ATTACK_USMASH"] = 63] = "ATTACK_USMASH"; State[State["ATTACK_DSMASH"] = 64] = "ATTACK_DSMASH"; State[State["AERIAL_NAIR"] = 65] = "AERIAL_NAIR"; State[State["AERIAL_FAIR"] = 66] = "AERIAL_FAIR"; State[State["AERIAL_BAIR"] = 67] = "AERIAL_BAIR"; State[State["AERIAL_UAIR"] = 68] = "AERIAL_UAIR"; State[State["AERIAL_DAIR"] = 69] = "AERIAL_DAIR"; // Weird GnW IDs State[State["GNW_JAB1"] = 341] = "GNW_JAB1"; State[State["GNW_JABM"] = 342] = "GNW_JABM"; State[State["GNW_DTILT"] = 345] = "GNW_DTILT"; State[State["GNW_FSMASH"] = 346] = "GNW_FSMASH"; State[State["GNW_NAIR"] = 347] = "GNW_NAIR"; State[State["GNW_BAIR"] = 348] = "GNW_BAIR"; State[State["GNW_UAIR"] = 349] = "GNW_UAIR"; // Peach FSMASH ID // FSMASH1 = Golf Club, FSMASH2 = Frying Pan, FSMASH3 = Tennis Racket State[State["PEACH_FSMASH1"] = 349] = "PEACH_FSMASH1"; State[State["PEACH_FSMASH2"] = 350] = "PEACH_FSMASH2"; State[State["PEACH_FSMASH3"] = 351] = "PEACH_FSMASH3"; // Command Grabs State[State["BARREL_WAIT"] = 293] = "BARREL_WAIT"; State[State["COMMAND_GRAB_RANGE1_START"] = 266] = "COMMAND_GRAB_RANGE1_START"; State[State["COMMAND_GRAB_RANGE1_END"] = 304] = "COMMAND_GRAB_RANGE1_END"; State[State["COMMAND_GRAB_RANGE2_START"] = 327] = "COMMAND_GRAB_RANGE2_START"; State[State["COMMAND_GRAB_RANGE2_END"] = 338] = "COMMAND_GRAB_RANGE2_END"; })(exports.State || (exports.State = {})); const Timers = { PUNISH_RESET_FRAMES: 45, RECOVERY_RESET_FRAMES: 45, COMBO_STRING_RESET_FRAMES: 45 }; function getSinglesPlayerPermutationsFromSettings(settings) { if (!settings || settings.players.length !== 2) { // Only return opponent indices for singles return []; } return [{ playerIndex: settings.players[0].playerIndex, opponentIndex: settings.players[1].playerIndex }, { playerIndex: settings.players[1].playerIndex, opponentIndex: settings.players[0].playerIndex }]; } function didLoseStock(frame, prevFrame) { if (!frame || !prevFrame) { return false; } return prevFrame.stocksRemaining - frame.stocksRemaining > 0; } function isInControl(state) { const ground = state >= exports.State.GROUNDED_CONTROL_START && state <= exports.State.GROUNDED_CONTROL_END; const squat = state >= exports.State.SQUAT_START && state <= exports.State.SQUAT_END; const groundAttack = state > exports.State.GROUND_ATTACK_START && state <= exports.State.GROUND_ATTACK_END; const isGrab = state === exports.State.GRAB; // TODO: Add grounded b moves? return ground || squat || groundAttack || isGrab; } function isTeching(state) { return state >= exports.State.TECH_START && state <= exports.State.TECH_END; } function isDown(state) { return state >= exports.State.DOWN_START && state <= exports.State.DOWN_END; } function isDamaged(state) { return state >= exports.State.DAMAGE_START && state <= exports.State.DAMAGE_END || state === exports.State.DAMAGE_FALL || state === exports.State.JAB_RESET_UP || state === exports.State.JAB_RESET_DOWN; } function isGrabbed(state) { return state >= exports.State.CAPTURE_START && state <= exports.State.CAPTURE_END; } // TODO: Find better implementation of 3 seperate ranges function isCommandGrabbed(state) { return (state >= exports.State.COMMAND_GRAB_RANGE1_START && state <= exports.State.COMMAND_GRAB_RANGE1_END || state >= exports.State.COMMAND_GRAB_RANGE2_START && state <= exports.State.COMMAND_GRAB_RANGE2_END) && state !== exports.State.BARREL_WAIT; } function isDead(state) { return state >= exports.State.DYING_START && state <= exports.State.DYING_END; } function calcDamageTaken(frame, prevFrame) { var _frame$percent, _prevFrame$percent; const percent = (_frame$percent = frame.percent) != null ? _frame$percent : 0; const prevPercent = (_prevFrame$percent = prevFrame.percent) != null ? _prevFrame$percent : 0; return percent - prevPercent; } // Frame pattern that indicates a dash dance turn was executed const dashDanceAnimations = [exports.State.DASH, exports.State.TURN, exports.State.DASH]; class ActionsComputer { constructor() { this.playerPermutations = new Array(); this.state = new Map(); } setup(settings) { this.state = new Map(); this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); this.playerPermutations.forEach(indices => { const playerCounts = { playerIndex: indices.playerIndex, wavedashCount: 0, wavelandCount: 0, airDodgeCount: 0, dashDanceCount: 0, spotDodgeCount: 0, ledgegrabCount: 0, rollCount: 0, lCancelCount: { success: 0, fail: 0 }, attackCount: { jab1: 0, jab2: 0, jab3: 0, jabm: 0, dash: 0, ftilt: 0, utilt: 0, dtilt: 0, fsmash: 0, usmash: 0, dsmash: 0, nair: 0, fair: 0, bair: 0, uair: 0, dair: 0 }, grabCount: { success: 0, fail: 0 }, throwCount: { up: 0, forward: 0, back: 0, down: 0 }, groundTechCount: { // tech away/in are in reference to the opponents position and not the stage away: 0, in: 0, neutral: 0, fail: 0 }, wallTechCount: { success: 0, fail: 0 } }; const playerState = { playerCounts: playerCounts, animations: [], actionFrameCounters: [] }; this.state.set(indices, playerState); }); } processFrame(frame) { this.playerPermutations.forEach(indices => { const state = this.state.get(indices); if (state) { handleActionCompute(state, indices, frame); } }); } fetch() { return Array.from(this.state.values()).map(val => val.playerCounts); } } function isMissGroundTech(animation) { return animation === exports.State.TECH_MISS_DOWN || animation === exports.State.TECH_MISS_UP; } function isRolling(animation) { return animation === exports.State.ROLL_BACKWARD || animation === exports.State.ROLL_FORWARD; } function isGrabAction(animation) { // Includes Grab pull, wait, pummel, and throws return animation > exports.State.GRAB && animation <= exports.State.THROW_DOWN && animation !== exports.State.DASH_GRAB; } function isGrabbing(animation) { return animation === exports.State.GRAB || animation === exports.State.DASH_GRAB; } function isAerialAttack(animation) { return animation >= exports.State.AERIAL_ATTACK_START && animation <= exports.State.AERIAL_ATTACK_END; } function isForwardTilt(animation) { return animation >= exports.State.ATTACK_FTILT_START && animation <= exports.State.ATTACK_FTILT_END; } function isForwardSmash(animation) { return animation >= exports.State.ATTACK_FSMASH_START && animation <= exports.State.ATTACK_FSMASH_END; } function handleActionCompute(state, indices, frame) { const playerFrame = frame.players[indices.playerIndex].post; const opponentFrame = frame.players[indices.opponentIndex].post; const incrementCount = (field, condition) => { if (!condition) { return; } const current = get(state.playerCounts, field, 0); set(state.playerCounts, field, current + 1); }; // Manage animation state const currentAnimation = playerFrame.actionStateId; state.animations.push(currentAnimation); const currentFrameCounter = playerFrame.actionStateCounter; state.actionFrameCounters.push(currentFrameCounter); // Grab last 3 frames const last3Frames = state.animations.slice(-3); const prevAnimation = last3Frames[last3Frames.length - 2]; const prevFrameCounter = state.actionFrameCounters[state.actionFrameCounters.length - 2]; // New action if new animation or frame counter goes back down (repeated action) const isNewAction = currentAnimation !== prevAnimation || prevFrameCounter > currentFrameCounter; if (!isNewAction) { return; } // Increment counts based on conditions const didDashDance = isEqual(last3Frames, dashDanceAnimations); incrementCount("dashDanceCount", didDashDance); incrementCount("rollCount", isRolling(currentAnimation)); incrementCount("spotDodgeCount", currentAnimation === exports.State.SPOT_DODGE); incrementCount("airDodgeCount", currentAnimation === exports.State.AIR_DODGE); incrementCount("ledgegrabCount", currentAnimation === exports.State.CLIFF_CATCH); // Grabs incrementCount("grabCount.success", isGrabbing(prevAnimation) && isGrabAction(currentAnimation)); incrementCount("grabCount.fail", isGrabbing(prevAnimation) && !isGrabAction(currentAnimation)); if (currentAnimation === exports.State.DASH_GRAB && prevAnimation === exports.State.ATTACK_DASH) { state.playerCounts.attackCount.dash -= 1; // subtract from dash attack if boost grab } // Basic attacks incrementCount("attackCount.jab1", currentAnimation === exports.State.ATTACK_JAB1); incrementCount("attackCount.jab2", currentAnimation === exports.State.ATTACK_JAB2); incrementCount("attackCount.jab3", currentAnimation === exports.State.ATTACK_JAB3); incrementCount("attackCount.jabm", currentAnimation === exports.State.ATTACK_JABM); incrementCount("attackCount.dash", currentAnimation === exports.State.ATTACK_DASH); incrementCount("attackCount.ftilt", isForwardTilt(currentAnimation)); incrementCount("attackCount.utilt", currentAnimation === exports.State.ATTACK_UTILT); incrementCount("attackCount.dtilt", currentAnimation === exports.State.ATTACK_DTILT); incrementCount("attackCount.fsmash", isForwardSmash(currentAnimation)); incrementCount("attackCount.usmash", currentAnimation === exports.State.ATTACK_USMASH); incrementCount("attackCount.dsmash", currentAnimation === exports.State.ATTACK_DSMASH); incrementCount("attackCount.nair", currentAnimation === exports.State.AERIAL_NAIR); incrementCount("attackCount.fair", currentAnimation === exports.State.AERIAL_FAIR); incrementCount("attackCount.bair", currentAnimation === exports.State.AERIAL_BAIR); incrementCount("attackCount.uair", currentAnimation === exports.State.AERIAL_UAIR); incrementCount("attackCount.dair", currentAnimation === exports.State.AERIAL_DAIR); // GnW is weird and has unique IDs for some moves if (playerFrame.internalCharacterId === 0x18) { incrementCount("attackCount.jab1", currentAnimation === exports.State.GNW_JAB1); incrementCount("attackCount.jabm", currentAnimation === exports.State.GNW_JABM); incrementCount("attackCount.dtilt", currentAnimation === exports.State.GNW_DTILT); incrementCount("attackCount.fsmash", currentAnimation === exports.State.GNW_FSMASH); incrementCount("attackCount.nair", currentAnimation === exports.State.GNW_NAIR); incrementCount("attackCount.bair", currentAnimation === exports.State.GNW_BAIR); incrementCount("attackCount.uair", currentAnimation === exports.State.GNW_UAIR); } // Peach is also weird and has a unique ID for her fsmash // FSMASH1 = Golf Club, FSMASH2 = Frying Pan, FSMASH3 = Tennis Racket if (playerFrame.internalCharacterId === 0x09) { incrementCount("attackCount.fsmash", currentAnimation === exports.State.PEACH_FSMASH1); incrementCount("attackCount.fsmash", currentAnimation === exports.State.PEACH_FSMASH2); incrementCount("attackCount.fsmash", currentAnimation === exports.State.PEACH_FSMASH3); } // Throws incrementCount("throwCount.up", currentAnimation === exports.State.THROW_UP); incrementCount("throwCount.forward", currentAnimation === exports.State.THROW_FORWARD); incrementCount("throwCount.down", currentAnimation === exports.State.THROW_DOWN); incrementCount("throwCount.back", currentAnimation === exports.State.THROW_BACK); // Techs const opponentDir = playerFrame.positionX > opponentFrame.positionX ? -1 : 1; const facingOpponent = playerFrame.facingDirection === opponentDir; incrementCount("groundTechCount.fail", isMissGroundTech(currentAnimation)); incrementCount("groundTechCount.in", currentAnimation === exports.State.FORWARD_TECH && facingOpponent); incrementCount("groundTechCount.in", currentAnimation === exports.State.BACKWARD_TECH && !facingOpponent); incrementCount("groundTechCount.neutral", currentAnimation === exports.State.NEUTRAL_TECH); incrementCount("groundTechCount.away", currentAnimation === exports.State.BACKWARD_TECH && facingOpponent); incrementCount("groundTechCount.away", currentAnimation === exports.State.FORWARD_TECH && !facingOpponent); incrementCount("wallTechCount.success", currentAnimation === exports.State.WALL_TECH); incrementCount("wallTechCount.fail", currentAnimation === exports.State.MISSED_WALL_TECH); if (isAerialAttack(currentAnimation)) { incrementCount("lCancelCount.success", playerFrame.lCancelStatus === 1); incrementCount("lCancelCount.fail", playerFrame.lCancelStatus === 2); } // Handles wavedash detection (and waveland) handleActionWavedash(state.playerCounts, state.animations); } function handleActionWavedash(counts, animations) { const currentAnimation = last(animations); const prevAnimation = animations[animations.length - 2]; const isSpecialLanding = currentAnimation === exports.State.LANDING_FALL_SPECIAL; const isAcceptablePrevious = isWavedashInitiationAnimation(prevAnimation); const isPossibleWavedash = isSpecialLanding && isAcceptablePrevious; if (!isPossibleWavedash) { return; } // Here we special landed, it might be a wavedash, let's check // We grab the last 8 frames here because that should be enough time to execute a // wavedash. This number could be tweaked if we find false negatives const recentFrames = animations.slice(-8); const recentAnimations = keyBy(recentFrames, animation => animation); if (size(recentAnimations) === 2 && recentAnimations[exports.State.AIR_DODGE]) { // If the only other animation is air dodge, this might be really late to the point // where it was actually an air dodge. Air dodge animation is really long return; } if (recentAnimations[exports.State.AIR_DODGE]) { // If one of the recent animations was an air dodge, let's remove that from the // air dodge counter, we don't want to count air dodges used to wavedash/land counts.airDodgeCount -= 1; } if (recentAnimations[exports.State.ACTION_KNEE_BEND]) { // If a jump was started recently, we will consider this a wavedash counts.wavedashCount += 1; } else { // If there was no jump recently, this is a waveland counts.wavelandCount += 1; } } function isWavedashInitiationAnimation(animation) { if (animation === exports.State.AIR_DODGE) { return true; } const isAboveMin = animation >= exports.State.CONTROLLED_JUMP_START; const isBelowMax = animation <= exports.State.CONTROLLED_JUMP_END; return isAboveMin && isBelowMax; } var ComboEvent; (function (ComboEvent) { ComboEvent["COMBO_START"] = "COMBO_START"; ComboEvent["COMBO_EXTEND"] = "COMBO_EXTEND"; ComboEvent["COMBO_END"] = "COMBO_END"; })(ComboEvent || (ComboEvent = {})); class ComboComputer extends events.EventEmitter { constructor(...args) { super(...args); this.playerPermutations = new Array(); this.state = new Map(); this.combos = new Array(); this.settings = null; } setup(settings) { // Reset the state this.settings = settings; this.state = new Map(); this.combos = []; this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); this.playerPermutations.forEach(indices => { const playerState = { combo: null, move: null, resetCounter: 0, lastHitAnimation: null, event: null }; this.state.set(indices, playerState); }); } processFrame(frame, allFrames) { this.playerPermutations.forEach(indices => { const state = this.state.get(indices); if (state) { handleComboCompute(allFrames, state, indices, frame, this.combos); // Emit an event for the new combo if (state.event !== null) { this.emit(state.event, { combo: last(this.combos), settings: this.settings }); state.event = null; } } }); } fetch() { return this.combos; } } function handleComboCompute(frames, state, indices, frame, combos) { const currentFrameNumber = frame.frame; const playerFrame = frame.players[indices.playerIndex].post; const opponentFrame = frame.players[indices.opponentIndex].post; const prevFrameNumber = currentFrameNumber - 1; let prevPlayerFrame = null; let prevOpponentFrame = null; if (frames[prevFrameNumber]) { prevPlayerFrame = frames[prevFrameNumber].players[indices.playerIndex].post; prevOpponentFrame = frames[prevFrameNumber].players[indices.opponentIndex].post; } const oppActionStateId = opponentFrame.actionStateId; const opntIsDamaged = isDamaged(oppActionStateId); const opntIsGrabbed = isGrabbed(oppActionStateId); const opntIsCommandGrabbed = isCommandGrabbed(oppActionStateId); const opntDamageTaken = prevOpponentFrame ? calcDamageTaken(opponentFrame, prevOpponentFrame) : 0; // Keep track of whether actionState changes after a hit. Used to compute move count // When purely using action state there was a bug where if you did two of the same // move really fast (such as ganon's jab), it would count as one move. Added // the actionStateCounter at this point which counts the number of frames since // an animation started. Should be more robust, for old files it should always be // null and null < null = false const actionChangedSinceHit = playerFrame.actionStateId !== state.lastHitAnimation; const actionCounter = playerFrame.actionStateCounter; const prevActionCounter = prevPlayerFrame ? prevPlayerFrame.actionStateCounter : 0; const actionFrameCounterReset = actionCounter < prevActionCounter; if (actionChangedSinceHit || actionFrameCounterReset) { state.lastHitAnimation = null; } // If opponent took damage and was put in some kind of stun this frame, either // start a combo or count the moves for the existing combo if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) { let comboStarted = false; if (!state.combo) { var _prevOpponentFrame$pe, _opponentFrame$percen; state.combo = { playerIndex: indices.opponentIndex, startFrame: currentFrameNumber, endFrame: null, startPercent: prevOpponentFrame ? (_prevOpponentFrame$pe = prevOpponentFrame.percent) != null ? _prevOpponentFrame$pe : 0 : 0, currentPercent: (_opponentFrame$percen = opponentFrame.percent) != null ? _opponentFrame$percen : 0, endPercent: null, moves: [], didKill: false, lastHitBy: indices.playerIndex }; combos.push(state.combo); // Track whether this is a new combo or not comboStarted = true; } if (opntDamageTaken) { // If animation of last hit has been cleared that means this is a new move. This // prevents counting multiple hits from the same move such as fox's drill if (state.lastHitAnimation === null) { state.move = { playerIndex: indices.playerIndex, frame: currentFrameNumber, moveId: playerFrame.lastAttackLanded, hitCount: 0, damage: 0 }; state.combo.moves.push(state.move); // Make sure we don't overwrite the START event if (!comboStarted) { state.event = ComboEvent.COMBO_EXTEND; } } if (state.move) { state.move.hitCount += 1; state.move.damage += opntDamageTaken; } // Store previous frame animation to consider the case of a trade, the previous // frame should always be the move that actually connected... I hope state.lastHitAnimation = prevPlayerFrame ? prevPlayerFrame.actionStateId : null; } if (comboStarted) { state.event = ComboEvent.COMBO_START; } } if (!state.combo) { // The rest of the function handles combo termination logic, so if we don't // have a combo started, there is no need to continue return; } const opntIsTeching = isTeching(oppActionStateId); const opntIsDowned = isDown(oppActionStateId); const opntDidLoseStock = prevOpponentFrame && didLoseStock(opponentFrame, prevOpponentFrame); const opntIsDying = isDead(oppActionStateId); // Update percent if opponent didn't lose stock if (!opntDidLoseStock) { var _opponentFrame$percen2; state.combo.currentPercent = (_opponentFrame$percen2 = opponentFrame.percent) != null ? _opponentFrame$percen2 : 0; } if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || opntIsTeching || opntIsDowned || opntIsDying) { // If opponent got grabbed or damaged, reset the reset counter state.resetCounter = 0; } else { state.resetCounter += 1; } let shouldTerminate = false; // Termination condition 1 - player kills opponent if (opntDidLoseStock) { state.combo.didKill = true; shouldTerminate = true; } // Termination condition 2 - combo resets on time if (state.resetCounter > Timers.COMBO_STRING_RESET_FRAMES) { shouldTerminate = true; } // If combo should terminate, mark the end states and add it to list if (shouldTerminate) { var _prevOpponentFrame$pe2; state.combo.endFrame = playerFrame.frame; state.combo.endPercent = prevOpponentFrame ? (_prevOpponentFrame$pe2 = prevOpponentFrame.percent) != null ? _prevOpponentFrame$pe2 : 0 : 0; state.event = ComboEvent.COMBO_END; state.combo = null; state.move = null; } } class ConversionComputer extends events.EventEmitter { constructor() { super(); this.playerPermutations = new Array(); this.conversions = new Array(); this.state = new Map(); this.metadata = void 0; this.settings = null; this.metadata = { lastEndFrameByOppIdx: {} }; } setup(settings) { // Reset the state this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); this.conversions = []; this.state = new Map(); this.metadata = { lastEndFrameByOppIdx: {} }; this.settings = settings; this.playerPermutations.forEach(indices => { const playerState = { conversion: null, move: null, resetCounter: 0, lastHitAnimation: null }; this.state.set(indices, playerState); }); } processFrame(frame, allFrames) { this.playerPermutations.forEach(indices => { const state = this.state.get(indices); if (state) { const terminated = handleConversionCompute(allFrames, state, indices, frame, this.conversions); if (terminated) { this.emit("CONVERSION", { combo: last(this.conversions), settings: this.settings }); } } }); } fetch() { this._populateConversionTypes(); return this.conversions; } _populateConversionTypes() { // Post-processing step: set the openingTypes const conversionsToHandle = filter(this.conversions, conversion => { return conversion.openingType === "unknown"; }); // Group new conversions by startTime and sort const groupedConversions = groupBy(conversionsToHandle, "startFrame"); const sortedConversions = orderBy(groupedConversions, conversions => get(conversions, [0, "startFrame"])); // Set the opening types on the conversions we need to handle sortedConversions.forEach(conversions => { const isTrade = conversions.length >= 2; conversions.forEach(conversion => { // Set end frame for this conversion this.metadata.lastEndFrameByOppIdx[conversion.playerIndex] = conversion.endFrame; if (isTrade) { // If trade, just short-circuit conversion.openingType = "trade"; return; } // If not trade, check the opponent endFrame const lastMove = last(conversion.moves); const oppEndFrame = this.metadata.lastEndFrameByOppIdx[lastMove ? lastMove.playerIndex : conversion.playerIndex]; const isCounterAttack = oppEndFrame && oppEndFrame > conversion.startFrame; conversion.openingType = isCounterAttack ? "counter-attack" : "neutral-win"; }); }); } } function handleConversionCompute(frames, state, indices, frame, conversions) { const currentFrameNumber = frame.frame; const playerFrame = frame.players[indices.playerIndex].post; const opponentFrame = frame.players[indices.opponentIndex].post; const prevFrameNumber = currentFrameNumber - 1; let prevPlayerFrame = null; let prevOpponentFrame = null; if (frames[prevFrameNumber]) { prevPlayerFrame = frames[prevFrameNumber].players[indices.playerIndex].post; prevOpponentFrame = frames[prevFrameNumber].players[indices.opponentIndex].post; } const oppActionStateId = opponentFrame.actionStateId; const opntIsDamaged = isDamaged(oppActionStateId); const opntIsGrabbed = isGrabbed(oppActionStateId); const opntIsCommandGrabbed = isCommandGrabbed(oppActionStateId); const opntDamageTaken = prevOpponentFrame ? calcDamageTaken(opponentFrame, prevOpponentFrame) : 0; // Keep track of whether actionState changes after a hit. Used to compute move count // When purely using action state there was a bug where if you did two of the same // move really fast (such as ganon's jab), it would count as one move. Added // the actionStateCounter at this point which counts the number of frames since // an animation started. Should be more robust, for old files it should always be // null and null < null = false const actionChangedSinceHit = playerFrame.actionStateId !== state.lastHitAnimation; const actionCounter = playerFrame.actionStateCounter; const prevActionCounter = prevPlayerFrame ? prevPlayerFrame.actionStateCou