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

Improved Conversion Logic #121

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
133 changes: 124 additions & 9 deletions src/stats/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { GameStartType, PostFrameUpdateType } from "../types";
import { Stage } from "../melee/types";

export interface StatsType {
gameComplete: boolean;
Expand Down Expand Up @@ -174,6 +175,8 @@ export enum State {
TECH_END = 0xcc,
DYING_START = 0x0,
DYING_END = 0xa,
LEDGE_ACTION_START = 0xfc,
LEDGE_ACTION_END = 0x107,
CONTROLLED_JUMP_START = 0x18,
CONTROLLED_JUMP_END = 0x22,
GROUND_ATTACK_START = 0x2c,
Expand All @@ -184,6 +187,12 @@ export enum State {
ATTACK_FTILT_END = 0x37,
ATTACK_FSMASH_START = 0x3a,
ATTACK_FSMASH_END = 0x3e,
GUARD_BREAK_START = 0xcd,
GUARD_BREAK_END = 0xd3,
DODGE_START = 0xe9,
DODGE_END = 0xec,
FALL_SPECIAL_START = 0x23,
FALL_SPECIAL_END = 0x25,

// Animation ID specific
ROLL_FORWARD = 0xe9,
Expand Down Expand Up @@ -289,15 +298,6 @@ export function didLoseStock(frame: PostFrameUpdateType, prevFrame: PostFrameUpd
return prevFrame.stocksRemaining! - frame.stocksRemaining! > 0;
}

export function isInControl(state: number): boolean {
const ground = state >= State.GROUNDED_CONTROL_START && state <= State.GROUNDED_CONTROL_END;
const squat = state >= State.SQUAT_START && state <= State.SQUAT_END;
const groundAttack = state > State.GROUND_ATTACK_START && state <= State.GROUND_ATTACK_END;
const isGrab = state === State.GRAB;
// TODO: Add grounded b moves?
return ground || squat || groundAttack || isGrab;
}

export function isTeching(state: number): boolean {
return state >= State.TECH_START && state <= State.TECH_END;
}
Expand All @@ -323,10 +323,125 @@ export function isCommandGrabbed(state: number): boolean {
);
}

export function isOffstage(
position: (number | null)[],
isAirborne: boolean | null,
currStage?: number | null,
): boolean {
if (!position || !currStage || isAirborne === false) {
//if isAirborne is null, run the check anyway for backwards compatibility
return false;
}
//-5 is below the main part of all legal stages. Just ignore the X value if the player is at or below this
if (position[1]! <= -5) {
return true;
}

let stageBounds = [0, 0];
switch (currStage) {
case Stage.FOUNTAIN_OF_DREAMS:
stageBounds = [-64, 64];
break;
case Stage.YOSHIS_STORY:
stageBounds = [-56, 56];
break;
case Stage.DREAMLAND:
stageBounds = [-73, 73];
break;
case Stage.POKEMON_STADIUM:
stageBounds = [-88, 88];
break;
case Stage.BATTLEFIELD:
stageBounds = [-67, 67];
break;
case Stage.FINAL_DESTINATION:
stageBounds = [-89, 89];
break;
default:
return false;
}
return position[0]! < stageBounds[0]! && position[0]! > stageBounds[1]!;
}

export function isDodging(state: number): boolean {
//not the greatest term, but captures rolling, spot dodging, and air dodging
return state >= State.DODGE_START && state <= State.DODGE_END;
}

export function isShielding(state: number): boolean {
return state >= State.GUARD_START && state <= State.GUARD_END;
}

export function isDead(state: number): boolean {
return state >= State.DYING_START && state <= State.DYING_END;
}

export function isShieldBroken(state: number): boolean {
return state >= State.GUARD_BREAK_START && state <= State.GUARD_BREAK_END;
}

export function isLedgeAction(state: number): boolean {
return state >= State.LEDGE_ACTION_START && state <= State.LEDGE_ACTION_END;
}

export function isMaybeJuggled(
position: (number | null)[],
isAirborne: boolean | null,
currStage?: number | null,
): boolean {
if (!position || !currStage || !isAirborne) {
return false;
}

let stageBounds = 0;

switch (currStage) {
case Stage.FOUNTAIN_OF_DREAMS:
stageBounds = 42;
break;
case Stage.YOSHIS_STORY:
stageBounds = 41;
break;
case Stage.DREAMLAND:
stageBounds = 51;
break;
case Stage.POKEMON_STADIUM:
//similar side plat heights to yoshi's, so we can steal the top plat height as well
stageBounds = 41;
break;
case Stage.BATTLEFIELD:
stageBounds = 54;
break;
case Stage.FINAL_DESTINATION:
//No plats, so we'll just use a lower-than-average value
stageBounds = 10; // or 45
break;
default:
return false;
}
return position[1]! >= stageBounds!;
}

export function isSpecialFall(state: number): boolean {
return state >= State.FALL_SPECIAL_START && state <= State.FALL_SPECIAL_END;
}

export function isUpBLag(state: number, prevState: number | null | undefined): boolean {
if (!state || !prevState) {
return false;
}
//allows resetting timer for land_fall_special without triggering due to wavedash/waveland
//specifically useful for up b's like sheik's that have a unique animation id for ~40 frames of the endlag
//rather than just going straight into fall_special -> land_fall_special
return (
state == State.LANDING_FALL_SPECIAL &&
prevState != State.LANDING_FALL_SPECIAL &&
prevState != State.ACTION_KNEE_BEND &&
prevState != State.AIR_DODGE &&
(prevState <= State.CONTROLLED_JUMP_START || prevState >= State.CONTROLLED_JUMP_END)
);
}

export function calcDamageTaken(frame: PostFrameUpdateType, prevFrame: PostFrameUpdateType): number {
const percent = frame.percent ?? 0;
const prevPercent = prevFrame.percent ?? 0;
Expand Down
71 changes: 57 additions & 14 deletions src/stats/conversions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import {
calcDamageTaken,
didLoseStock,
getSinglesPlayerPermutationsFromSettings,
isShielding,
isShieldBroken,
isDodging,
isTeching,
isOffstage,
isDead,
isDown,
isLedgeAction,
isCommandGrabbed,
isDamaged,
isGrabbed,
isInControl,
isMaybeJuggled,
isSpecialFall,
isUpBLag,
Timers,
} from "./common";
import type { StatComputer } from "./stats";
Expand Down Expand Up @@ -67,7 +77,14 @@ export class ConversionComputer extends EventEmitter implements StatComputer<Con
this.playerPermutations.forEach((indices) => {
const state = this.state.get(indices);
if (state) {
const terminated = handleConversionCompute(allFrames, state, indices, frame, this.conversions);
const terminated = handleConversionCompute(
allFrames,
state,
indices,
frame,
this.conversions,
this.settings?.stageId,
);
if (terminated) {
this.emit("CONVERSION", {
combo: last(this.conversions),
Expand Down Expand Up @@ -123,6 +140,7 @@ function handleConversionCompute(
indices: PlayerIndexedType,
frame: FrameEntryType,
conversions: ConversionType[],
stageId: number | null | undefined,
): boolean {
const currentFrameNumber = frame.frame;
const playerFrame: PostFrameUpdateType = frame.players[indices.playerIndex]!.post;
Expand Down Expand Up @@ -158,7 +176,7 @@ function handleConversionCompute(
}

// If opponent took damage and was put in some kind of stun this frame, either
// start a conversion or
// start a conversion or continue the current conversion
if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) {
if (!state.conversion) {
state.conversion = {
Expand Down Expand Up @@ -209,36 +227,61 @@ function handleConversionCompute(
return false;
}

const opntInControl = isInControl(oppActionStateId);
const opntDidLoseStock = prevOpponentFrame && didLoseStock(opponentFrame, prevOpponentFrame);
const playerDidLoseStock = prevPlayerFrame && didLoseStock(playerFrame, prevPlayerFrame);

const opntPosition = [opponentFrame.positionX, opponentFrame.positionY];
const opntIsOffstage = isOffstage(opntPosition, opponentFrame.isAirborne, stageId);
const opntIsDodging = isDodging(oppActionStateId);
const opntIsShielding = isShielding(oppActionStateId);
const opntIsTeching = isTeching(oppActionStateId);
const opntIsDowned = isDown(oppActionStateId);
const opntIsShieldBroken = isShieldBroken(oppActionStateId);
const opntIsDying = isDead(oppActionStateId);
const opntIsLedgeAction = isLedgeAction(oppActionStateId);
const opntIsMaybeJuggled = isMaybeJuggled(opntPosition, opponentFrame.isAirborne, stageId);
const opntIsSpecialFall = isSpecialFall(oppActionStateId);
const opntIsUpBLag = isUpBLag(oppActionStateId, prevOpponentFrame?.actionStateId);

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

if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) {
if (
opntIsDamaged ||
opntIsGrabbed ||
opntIsCommandGrabbed ||
opntIsTeching ||
opntIsDowned ||
opntIsDying ||
opntIsOffstage ||
opntIsDodging ||
opntIsShielding ||
opntIsShieldBroken ||
opntIsLedgeAction ||
opntIsMaybeJuggled ||
opntIsSpecialFall ||
opntIsUpBLag
) {
// 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
} else {
state.resetCounter += 1;
}

let shouldTerminate = false;

// Termination condition 1 - player kills opponent
// Termination condition 1 - player kills opponent or opponent kills player
if (opntDidLoseStock) {
state.conversion.didKill = true;
shouldTerminate = true;
}

if (playerDidLoseStock) {
shouldTerminate = true;
}

// Termination condition 2 - conversion resets on time
if (state.resetCounter > Timers.PUNISH_RESET_FRAMES) {
shouldTerminate = true;
Expand Down