UNPKG

slp-enforcer

Version:

Finds violations of the Melee Controller Ruleset by inspecting SLP files

202 lines (201 loc) 9.54 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const filter_1 = __importDefault(require("lodash/filter")); const get_1 = __importDefault(require("lodash/get")); const groupBy_1 = __importDefault(require("lodash/groupBy")); const last_1 = __importDefault(require("lodash/last")); const orderBy_1 = __importDefault(require("lodash/orderBy")); const common_1 = require("./common"); class ConversionComputer extends events_1.EventEmitter { constructor() { super(); this.playerPermutations = new Array(); this.conversions = new Array(); this.state = new Map(); this.settings = null; this.metadata = { lastEndFrameByOppIdx: {}, }; } setup(settings) { // Reset the state this.playerPermutations = common_1.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_1.default(this.conversions), settings: this.settings, }); } } }); } fetch() { this._populateConversionTypes(); return this.conversions; } _populateConversionTypes() { // Post-processing step: set the openingTypes const conversionsToHandle = filter_1.default(this.conversions, (conversion) => { return conversion.openingType === "unknown"; }); // Group new conversions by startTime and sort const groupedConversions = groupBy_1.default(conversionsToHandle, "startFrame"); const sortedConversions = orderBy_1.default(groupedConversions, (conversions) => get_1.default(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_1.default(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"; }); }); } } exports.ConversionComputer = ConversionComputer; function handleConversionCompute(frames, state, indices, frame, conversions) { var _a, _b, _c, _d; 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 = common_1.isDamaged(oppActionStateId); const opntIsGrabbed = common_1.isGrabbed(oppActionStateId); const opntIsCommandGrabbed = common_1.isCommandGrabbed(oppActionStateId); const opntDamageTaken = prevOpponentFrame ? common_1.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 conversion or if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) { if (!state.conversion) { state.conversion = { playerIndex: indices.opponentIndex, lastHitBy: indices.playerIndex, startFrame: currentFrameNumber, endFrame: null, startPercent: prevOpponentFrame ? (_a = prevOpponentFrame.percent) !== null && _a !== void 0 ? _a : 0 : 0, currentPercent: (_b = opponentFrame.percent) !== null && _b !== void 0 ? _b : 0, endPercent: null, moves: [], didKill: false, openingType: "unknown", }; conversions.push(state.conversion); } 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.conversion.moves.push(state.move); } 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 (!state.conversion) { // The rest of the function handles conversion termination logic, so if we don't // have a conversion started, there is no need to continue return false; } const opntInControl = common_1.isInControl(oppActionStateId); const opntDidLoseStock = prevOpponentFrame && common_1.didLoseStock(opponentFrame, prevOpponentFrame); // Update percent if opponent didn't lose stock if (!opntDidLoseStock) { state.conversion.currentPercent = (_c = opponentFrame.percent) !== null && _c !== void 0 ? _c : 0; } if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) { // If opponent got grabbed or damaged, reset the reset counter state.resetCounter = 0; } const shouldStartResetCounter = state.resetCounter === 0 && opntInControl; const shouldContinueResetCounter = state.resetCounter > 0; if (shouldStartResetCounter || shouldContinueResetCounter) { // This will increment the reset timer under the following conditions: // 1) if we were punishing opponent but they have now entered an actionable state // 2) if counter has already started counting meaning opponent has entered actionable state state.resetCounter += 1; } let shouldTerminate = false; // Termination condition 1 - player kills opponent if (opntDidLoseStock) { state.conversion.didKill = true; shouldTerminate = true; } // Termination condition 2 - conversion resets on time if (state.resetCounter > common_1.Timers.PUNISH_RESET_FRAMES) { shouldTerminate = true; } // If conversion should terminate, mark the end states and add it to list if (shouldTerminate) { state.conversion.endFrame = playerFrame.frame; state.conversion.endPercent = prevOpponentFrame ? (_d = prevOpponentFrame.percent) !== null && _d !== void 0 ? _d : 0 : 0; state.conversion = null; state.move = null; } return shouldTerminate; }