import { EventEmitter } from "events" ; import _ from "lodash" ; import type { FrameEntryType , FramesType , GameStartType , PostFrameUpdateType } from "../types" ; import type { ComboType , MoveLandedType , PlayerIndexedType } from "./common" ; import { calcDamageTaken , didLoseStock , getSinglesPlayerPermutationsFromSettings , isCommandGrabbed , isDamaged , isDead , isDown , isGrabbed , isTeching , Timers , } from "./common" ; import type { StatComputer } from "./stats" ; export enum ComboEvent { COMBO_START = "COMBO_START" , COMBO_EXTEND = "COMBO_EXTEND" , COMBO_END = "COMBO_END" } interface ComboState { combo : ComboType | null ; move : MoveLandedType | null ; resetCounter : number ; lastHitAnimation : number | null ; event : ComboEvent | null ; } export class ComboComputer extends EventEmitter implements StatComputer < ComboType [ ] > { private playerPermutations = new Array < PlayerIndexedType > ( ) ; private state = new Map < PlayerIndexedType , ComboState > ( ) ; private combos = new Array < ComboType > ( ) ; private settings : GameStartType | null = null ; public setup ( settings ) { 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 ) ; } ) ; } public processFrame ( frame , allFrames ) { this . playerPermutations . forEach ( ( indices ) => { const state = this . state . get ( indices ) ; if ( state ) { handleComboCompute ( allFrames , state , indices , frame , this . combos ) ; if ( state . event !== null ) { this . emit ( state . event , { combo : _ . last ( this . combos ) , settings : this . settings , } ) ; state . event = null ; } } } ) ; } public 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 ; 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 ( opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed ) { let comboStarted = false ; if ( ! state . combo ) { state . combo = { playerIndex : indices . opponentIndex , startFrame : currentFrameNumber , endFrame : null , startPercent : prevOpponentFrame ? prevOpponentFrame . percent ?? 0 : 0 , currentPercent : opponentFrame . percent ?? 0 , endPercent : null , moves : [ ] , didKill : false , lastHitBy : indices . playerIndex , } ; combos . push ( state . combo ) ; comboStarted = true ; } if ( opntDamageTaken ) { if ( state . lastHitAnimation === null ) { state . move = { playerIndex : indices . playerIndex , frame : currentFrameNumber , moveId : playerFrame . lastAttackLanded ! , hitCount : 0 , damage : 0 , } ; state . combo . moves . push ( state . move ) ; if ( ! comboStarted ) { state . event = ComboEvent . COMBO_EXTEND ; } } if ( state . move ) { state . move . hitCount += 1 ; state . move . damage += opntDamageTaken ; } state . lastHitAnimation = prevPlayerFrame ? prevPlayerFrame . actionStateId : null ; } if ( comboStarted ) { state . event = ComboEvent . COMBO_START ; } } if ( ! state . combo ) { return ; } const opntIsTeching = isTeching ( oppActionStateId ) ; const opntIsDowned = isDown ( oppActionStateId ) ; const opntDidLoseStock = prevOpponentFrame && didLoseStock ( opponentFrame , prevOpponentFrame ) ; const opntIsDying = isDead ( oppActionStateId ) ; if ( ! opntDidLoseStock ) { state . combo . currentPercent = opponentFrame . percent ?? 0 ; } if ( opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || opntIsTeching || opntIsDowned || opntIsDying ) { state . resetCounter = 0 ; } else { state . resetCounter += 1 ; } let shouldTerminate = false ; if ( opntDidLoseStock ) { state . combo . didKill = true ; shouldTerminate = true ; } if ( state . resetCounter > Timers . COMBO_STRING_RESET_FRAMES ) { shouldTerminate = true ; } if ( shouldTerminate ) { state . combo . endFrame = playerFrame . frame ; state . combo . endPercent = prevOpponentFrame ? prevOpponentFrame . percent ?? 0 : 0 ; state . event = ComboEvent . COMBO_END ; state . combo = null ; state . move = null ; } }