Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Doubles Stats (#54)" #71

Merged
merged 2 commits into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions src/stats/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from "lodash";

import { FrameEntryType, GameStartType } from "../types";
import { ActionCountsType, State } from "./common";
import { ActionCountsType, getSinglesPlayerPermutationsFromSettings, PlayerIndexedType, State } from "./common";
import { StatComputer } from "./stats";

// Frame pattern that indicates a dash dance turn was executed
Expand All @@ -13,17 +13,16 @@ interface PlayerActionState {
}

export class ActionsComputer implements StatComputer<ActionCountsType[]> {
private playerIndices: number[] = [];
private state = new Map<number, PlayerActionState>();
private playerPermutations = new Array<PlayerIndexedType>();
private state = new Map<PlayerIndexedType, PlayerActionState>();

public setup(settings: GameStartType): void {
// Reset the state
this.state = new Map();

this.playerIndices = settings.players.map((p) => p.playerIndex);
this.playerIndices.forEach((playerIndex) => {
this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings);
this.playerPermutations.forEach((indices) => {
const playerCounts: ActionCountsType = {
playerIndex,
playerIndex: indices.playerIndex,
opponentIndex: indices.opponentIndex,
wavedashCount: 0,
wavelandCount: 0,
airDodgeCount: 0,
Expand All @@ -50,15 +49,15 @@ export class ActionsComputer implements StatComputer<ActionCountsType[]> {
playerCounts: playerCounts,
animations: [],
};
this.state.set(playerIndex, playerState);
this.state.set(indices, playerState);
});
}

public processFrame(frame: FrameEntryType): void {
this.playerIndices.forEach((index) => {
const state = this.state.get(index);
this.playerPermutations.forEach((indices) => {
const state = this.state.get(indices);
if (state) {
handleActionCompute(state, index, frame);
handleActionCompute(state, indices, frame);
}
});
}
Expand Down Expand Up @@ -123,8 +122,8 @@ function didStartLedgegrab(currentAnimation: State, previousAnimation: State): b
return isCurrentlyGrabbingLedge && !wasPreviouslyGrabbingLedge;
}

function handleActionCompute(state: PlayerActionState, playerIndex: number, frame: FrameEntryType): void {
const playerFrame = frame.players[playerIndex]!.post;
function handleActionCompute(state: PlayerActionState, indices: PlayerIndexedType, frame: FrameEntryType): void {
const playerFrame = frame.players[indices.playerIndex]!.post;
const incrementCount = (field: string, condition: boolean): void => {
if (!condition) {
return;
Expand Down
99 changes: 45 additions & 54 deletions src/stats/combos.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { EventEmitter } from "events";
import { last } from "lodash";
import _ from "lodash";

import { FrameEntryType, FramesType, GameStartType, PostFrameUpdateType } from "../types";
import {
calcDamageTaken,
ComboType,
didLoseStock,
getSinglesPlayerPermutationsFromSettings,
isCommandGrabbed,
isDamaged,
isDead,
isDown,
isGrabbed,
isTeching,
MoveLandedType,
PlayerIndexedType,
Timers,
} from "./common";
import { StatComputer } from "./stats";
Expand All @@ -32,19 +34,19 @@ interface ComboState {
}

export class ComboComputer extends EventEmitter implements StatComputer<ComboType[]> {
private playerIndices: number[] = [];
private combos: ComboType[] = [];
private state = new Map<number, ComboState>();
private playerPermutations = new Array<PlayerIndexedType>();
private state = new Map<PlayerIndexedType, ComboState>();
private combos = new Array<ComboType>();
private settings: GameStartType | null = null;

public setup(settings: GameStartType): void {
// Reset the state
this.settings = settings;
this.state = new Map();
this.combos = [];
this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings);

this.playerIndices = settings.players.map((p) => p.playerIndex);
this.playerIndices.forEach((indices) => {
this.playerPermutations.forEach((indices) => {
const playerState: ComboState = {
combo: null,
move: null,
Expand All @@ -57,15 +59,15 @@ export class ComboComputer extends EventEmitter implements StatComputer<ComboTyp
}

public processFrame(frame: FrameEntryType, allFrames: FramesType): void {
this.playerIndices.forEach((index) => {
const state = this.state.get(index);
this.playerPermutations.forEach((indices) => {
const state = this.state.get(indices);
if (state) {
handleComboCompute(allFrames, state, index, frame, this.combos);
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),
combo: _.last(this.combos),
settings: this.settings,
});
state.event = null;
Expand All @@ -82,20 +84,29 @@ export class ComboComputer extends EventEmitter implements StatComputer<ComboTyp
function handleComboCompute(
frames: FramesType,
state: ComboState,
playerIndex: number,
indices: PlayerIndexedType,
frame: FrameEntryType,
combos: ComboType[],
): void {
const currentFrameNumber = frame.frame;
const playerFrame = frame.players[playerIndex]!.post;
const playerFrame = frame.players[indices.playerIndex]!.post;
const opponentFrame = frame.players[indices.opponentIndex]!.post;

const prevFrameNumber = currentFrameNumber - 1;
let prevPlayerFrame: PostFrameUpdateType | null = null;
let prevOpponentFrame: PostFrameUpdateType | null = null;

if (frames[prevFrameNumber]) {
prevPlayerFrame = frames[prevFrameNumber].players[playerIndex]!.post;
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
Expand All @@ -110,27 +121,21 @@ function handleComboCompute(
state.lastHitAnimation = null;
}

const playerActionStateId = playerFrame.actionStateId!;
const playerIsDamaged = isDamaged(playerActionStateId);
const playerIsGrabbed = isGrabbed(playerActionStateId);
const playerIsCommandGrabbed = isCommandGrabbed(playerActionStateId);

// If the player took damage and was put in some kind of stun this frame, either
// 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 (playerIsDamaged || playerIsGrabbed || playerIsCommandGrabbed) {
if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) {
let comboStarted = false;

if (!state.combo) {
state.combo = {
playerIndex,
playerIndex: indices.playerIndex,
opponentIndex: indices.opponentIndex,
startFrame: currentFrameNumber,
endFrame: null,
startPercent: prevPlayerFrame ? prevPlayerFrame.percent ?? 0 : 0,
currentPercent: playerFrame.percent ?? 0,
startPercent: prevOpponentFrame ? prevOpponentFrame.percent ?? 0 : 0,
currentPercent: opponentFrame.percent ?? 0,
endPercent: null,
moves: [],
didKill: false,
lastHitBy: null,
};

combos.push(state.combo);
Expand All @@ -139,22 +144,15 @@ function handleComboCompute(
comboStarted = true;
}

const playerDamageTaken = prevPlayerFrame ? calcDamageTaken(playerFrame, prevPlayerFrame) : 0;
const lastHitBy = playerFrame.lastHitBy;
const validLastHitBy = lastHitBy !== null && lastHitBy >= 0 && lastHitBy <= 3;
if (playerDamageTaken && lastHitBy !== null && validLastHitBy) {
// Update who hit us last
state.combo.lastHitBy = lastHitBy;

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 = {
frame: currentFrameNumber,
moveId: frame.players[lastHitBy]!.post!.lastAttackLanded!,
moveId: playerFrame.lastAttackLanded!,
hitCount: 0,
damage: 0,
playerIndex: lastHitBy,
};

state.combo.moves.push(state.move);
Expand All @@ -167,7 +165,7 @@ function handleComboCompute(

if (state.move) {
state.move.hitCount += 1;
state.move.damage += playerDamageTaken;
state.move.damage += opntDamageTaken;
}

// Store previous frame animation to consider the case of a trade, the previous
Expand All @@ -186,34 +184,27 @@ function handleComboCompute(
return;
}

const playerIsTeching = isTeching(playerActionStateId);
const playerIsDowned = isDown(playerActionStateId);
const playerDidLoseStock = prevPlayerFrame && didLoseStock(playerFrame, prevPlayerFrame);
const playerIsDying = isDead(playerActionStateId);
const opntIsTeching = isTeching(oppActionStateId);
const opntIsDowned = isDown(oppActionStateId);
const opntDidLoseStock = prevOpponentFrame && didLoseStock(opponentFrame, prevOpponentFrame);
const opntIsDying = isDead(oppActionStateId);

// Update percent if the player didn't lose stock
if (!playerDidLoseStock) {
state.combo.currentPercent = playerFrame.percent ?? 0;
// Update percent if opponent didn't lose stock
if (!opntDidLoseStock) {
state.combo.currentPercent = opponentFrame.percent ?? 0;
}

if (
playerIsDamaged ||
playerIsGrabbed ||
playerIsCommandGrabbed ||
playerIsTeching ||
playerIsDowned ||
playerIsDying
) {
// If the player got grabbed or damaged, reset the reset counter
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 was killed
if (playerDidLoseStock) {
// Termination condition 1 - player kills opponent
if (opntDidLoseStock) {
state.combo.didKill = true;
shouldTerminate = true;
}
Expand All @@ -226,7 +217,7 @@ function handleComboCompute(
// If combo should terminate, mark the end states and add it to list
if (shouldTerminate) {
state.combo.endFrame = playerFrame.frame;
state.combo.endPercent = prevPlayerFrame ? prevPlayerFrame.percent ?? 0 : 0;
state.combo.endPercent = prevOpponentFrame ? prevOpponentFrame.percent ?? 0 : 0;
state.event = ComboEvent.COMBO_END;

state.combo = null;
Expand Down
54 changes: 33 additions & 21 deletions src/stats/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from "lodash";

import { PostFrameUpdateType } from "../types";
import { GameStartType, PostFrameUpdateType } from "../types";

export interface StatsType {
gameComplete: boolean;
Expand All @@ -19,6 +19,11 @@ export interface RatioType {
ratio: number | null;
}

export interface PlayerIndexedType {
playerIndex: number;
opponentIndex: number;
}

export interface DurationType {
startFrame: number;
endFrame?: number | null;
Expand All @@ -30,8 +35,7 @@ export interface DamageType {
endPercent?: number | null;
}

export interface StockType extends DurationType, DamageType {
playerIndex: number;
export interface StockType extends PlayerIndexedType, DurationType, DamageType {
count: number;
deathAnimation?: number | null;
}
Expand All @@ -41,22 +45,20 @@ export interface MoveLandedType {
moveId: number;
hitCount: number;
damage: number;
playerIndex: number;
}

export interface ComboType extends DurationType, DamageType {
playerIndex: number;
export interface ConversionType extends PlayerIndexedType, DurationType, DamageType {
moves: MoveLandedType[];
openingType: string;
didKill: boolean;
lastHitBy: number | null;
}

export interface ConversionType extends ComboType {
openingType: string;
export interface ComboType extends PlayerIndexedType, DurationType, DamageType {
moves: MoveLandedType[];
didKill: boolean;
}

export interface ActionCountsType {
playerIndex: number;
export interface ActionCountsType extends PlayerIndexedType {
wavedashCount: number;
wavelandCount: number;
airDodgeCount: number;
Expand Down Expand Up @@ -88,8 +90,7 @@ export interface InputCountsType {
total: number;
}

export interface OverallType {
playerIndex: number;
export interface OverallType extends PlayerIndexedType {
inputCounts: InputCountsType;
conversionCount: number;
totalDamage: number;
Expand Down Expand Up @@ -175,19 +176,30 @@ export const Timers = {
COMBO_STRING_RESET_FRAMES: 45,
};

export function didLoseStock(
frame: PostFrameUpdateType | undefined,
prevFrame: PostFrameUpdateType | undefined,
): boolean {
if (!frame || !prevFrame) {
return false;
export function getSinglesPlayerPermutationsFromSettings(settings: GameStartType): PlayerIndexedType[] {
if (!settings || settings.players.length !== 2) {
// Only return opponent indices for singles
return [];
}

if (prevFrame.stocksRemaining === null || frame.stocksRemaining === null) {
return [
{
playerIndex: settings.players[0].playerIndex,
opponentIndex: settings.players[1].playerIndex,
},
{
playerIndex: settings.players[1].playerIndex,
opponentIndex: settings.players[0].playerIndex,
},
];
}

export function didLoseStock(frame: PostFrameUpdateType, prevFrame: PostFrameUpdateType): boolean {
if (!frame || !prevFrame) {
return false;
}

return prevFrame.stocksRemaining - frame.stocksRemaining > 0;
return prevFrame.stocksRemaining! - frame.stocksRemaining! > 0;
}

export function isInControl(state: number): boolean {
Expand Down
Loading