import { EventEmitter } from "events" ; import _ from "lodash" ; import type { FrameEntryType , FramesType , GameStartType , PostFrameUpdateType } from "../types" ; import type { ConversionType , MoveLandedType , PlayerIndexedType } from "./common" ; import { calcDamageTaken , didLoseStock , getSinglesPlayerPermutationsFromSettings , isCommandGrabbed , isDamaged , isGrabbed , isInControl , Timers , } from "./common" ; import type { StatComputer } from "./stats" ; interface PlayerConversionState { conversion : ConversionType | null ; move : MoveLandedType | null ; resetCounter : number ; lastHitAnimation : number | null ; } interface MetadataType { lastEndFrameByOppIdx : { [ oppIdx ] : number ; } ; } export class ConversionComputer extends EventEmitter implements StatComputer < ConversionType [ ] > { private playerPermutations = new Array < PlayerIndexedType > ( ) ; private conversions = new Array < ConversionType > ( ) ; private state = new Map < PlayerIndexedType , PlayerConversionState > ( ) ; private metadata : MetadataType ; private settings : GameStartType | null = null ; public constructor ( ) { super ( ) ; this . metadata = { lastEndFrameByOppIdx : { } , } ; } public setup ( settings ) { 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 ) ; } ) ; } public 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 , } ) ; } } } ) ; } public fetch ( ) { this . _populateConversionTypes ( ) ; return this . conversions ; } private _populateConversionTypes ( ) { const conversionsToHandle = _ . filter ( this . conversions , ( conversion ) => { return conversion . openingType === "unknown" ; } ) ; const sortedConversions = _ . chain ( conversionsToHandle ) . groupBy ( "startFrame" ) . orderBy ( ( conversions ) => _ . get ( conversions , [ 0 , "startFrame" ] ) ) . value ( ) ; sortedConversions . forEach ( ( conversions ) => { const isTrade = conversions . length >= 2 ; conversions . forEach ( ( conversion ) => { this . metadata . lastEndFrameByOppIdx [ conversion . playerIndex ] = conversion . endFrame ! ; if ( isTrade ) { conversion . openingType = "trade" ; return ; } 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 ; 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 ) { if ( ! state . conversion ) { state . conversion = { playerIndex : indices . opponentIndex , lastHitBy : indices . playerIndex , startFrame : currentFrameNumber , endFrame : null , startPercent : prevOpponentFrame ? prevOpponentFrame . percent ?? 0 : 0 , currentPercent : opponentFrame . percent ?? 0 , endPercent : null , moves : [ ] , didKill : false , openingType : "unknown" , } ; conversions . push ( state . conversion ) ; } if ( opntDamageTaken ) { if ( state . lastHitAnimation === null ) { state . move = { playerIndex : indices . playerIndex , frame : currentFrameNumber , moveId : playerFrame . lastAttackLanded ! , hitCount : 0 , damage : 0 , } ; state . conversion . moves . push ( state . move ) ; } if ( state . move ) { state . move . hitCount += 1 ; state . move . damage += opntDamageTaken ; } state . lastHitAnimation = prevPlayerFrame ? prevPlayerFrame . actionStateId : null ; } } if ( ! state . conversion ) { return false ; } const opntInControl = isInControl ( oppActionStateId ) ; const opntDidLoseStock = prevOpponentFrame && didLoseStock ( opponentFrame , prevOpponentFrame ) ; if ( ! opntDidLoseStock ) { state . conversion . currentPercent = opponentFrame . percent ?? 0 ; } if ( opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed ) { state . resetCounter = 0 ; } const shouldStartResetCounter = state . resetCounter === 0 && opntInControl ; const shouldContinueResetCounter = state . resetCounter > 0 ; if ( shouldStartResetCounter || shouldContinueResetCounter ) { state . resetCounter += 1 ; } let shouldTerminate = false ; if ( opntDidLoseStock ) { state . conversion . didKill = true ; shouldTerminate = true ; } if ( state . resetCounter > Timers . PUNISH_RESET_FRAMES ) { shouldTerminate = true ; } if ( shouldTerminate ) { state . conversion . endFrame = playerFrame . frame ; state . conversion . endPercent = prevOpponentFrame ? prevOpponentFrame . percent ?? 0 : 0 ; state . conversion = null ; state . move = null ; } return shouldTerminate ; }