Made adjustments based on reviewer suggestions

This commit is contained in:
jnotsknab 2025-06-22 23:12:23 -05:00
parent c4f1ec2a7a
commit 87046712af
10 changed files with 61 additions and 74 deletions

View File

@ -800,7 +800,7 @@ export default class BattleScene extends SceneBase {
// TODO: Add `undefined` to return type // TODO: Add `undefined` to return type
/** /**
* Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not. * Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not.
* @param active - If true, returns only the PlayerPokemon that are currently active * @param active - Whether to consider only active on-field pokemon ({@see {@linkcode Pokemon.isActiveZ} for more information}); default `false`
* @returns array of {@linkcode PlayerPokemon} * @returns array of {@linkcode PlayerPokemon}
*/ */
public getPlayerField(active = false): PlayerPokemon[] { public getPlayerField(active = false): PlayerPokemon[] {

View File

@ -94,7 +94,10 @@ export default class Battle {
/** If the current battle is a Mystery Encounter, this will always be defined */ /** If the current battle is a Mystery Encounter, this will always be defined */
public mysteryEncounter?: MysteryEncounter; public mysteryEncounter?: MysteryEncounter;
/**Tracks whether the last run attempt in battle failed*/ /**
* Tracker for whether the last run attempt failed.
* @defaultValue `false`
*/
public failedRunAway = false; public failedRunAway = false;
private rngCounter = 0; private rngCounter = 0;

View File

@ -118,7 +118,13 @@ class DefaultOverrides {
* or `false` to force it to never trigger. * or `false` to force it to never trigger.
*/ */
readonly CONFUSION_ACTIVATION_OVERRIDE: boolean | null = null; readonly CONFUSION_ACTIVATION_OVERRIDE: boolean | null = null;
/**
* If non-null, will override random flee attempts to always or never succeed by forcing {@linkcode calculateEscapeChance} to return 100% or 0%.
* Set to `null` to disable.
*
* Is overridden if either player Pokemon has {@linkcode AbilityId.RUN_AWAY | Run Away}.
*/
readonly RUN_SUCCESS_OVERRIDE: boolean | null = null;
// ---------------- // ----------------
// PLAYER OVERRIDES // PLAYER OVERRIDES
// ---------------- // ----------------

View File

@ -1,38 +1,33 @@
import { applyAbAttrs, applyPreLeaveFieldAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs, applyPreLeaveFieldAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { NumberHolder, randSeedInt } from "#app/utils/common";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { calculateEscapeChance } from "#app/utils/run-utils"; import { calculateEscapeChance } from "#app/utils/run-utils";
import { FieldPhase } from "./field-phase"; import { FieldPhase } from "./field-phase";
export class AttemptRunPhase extends FieldPhase { export class AttemptRunPhase extends FieldPhase {
public readonly phaseName = "AttemptRunPhase"; public readonly phaseName = "AttemptRunPhase";
/** For testing purposes: this is to force the pokemon to fail and escape */
public forceFailEscape = false;
private getTeamRNG(range: number, min = 0) { private getTeamRNG(range: number, min = 0) {
return globalScene.currentBattle ? globalScene.randBattleSeedInt(range, min) : randSeedInt(range, min); return globalScene.randBattleSeedInt(range, min);
} }
start() { start() {
super.start(); super.start();
//Increment escape attempts count on entry // Increment escape attempts count on entry
const currentAttempts = globalScene.currentBattle.escapeAttempts++; const currentAttempts = globalScene.currentBattle.escapeAttempts++;
const activePlayerField = globalScene.getPlayerField(true); const activePlayerField = globalScene.getPlayerField(true);
const enemyField = globalScene.getEnemyField(); const enemyField = globalScene.getEnemyField();
const escapeRoll = this.getTeamRNG(100); const escapeRoll = this.getTeamRNG(100);
const escapeChance = new NumberHolder(0); const escapeChance = calculateEscapeChance(currentAttempts);
calculateEscapeChance(activePlayerField, enemyField, escapeChance, currentAttempts);
activePlayerField.forEach(p => { activePlayerField.forEach(p => {
applyAbAttrs("RunSuccessAbAttr", p, null, false, escapeChance); applyAbAttrs("RunSuccessAbAttr", p, null, false, { value: escapeChance });
}); });
if (escapeRoll < escapeChance.value && !this.forceFailEscape) { if (escapeRoll < escapeChance) {
enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", enemyPokemon)); enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", enemyPokemon));
globalScene.playSound("se/flee"); globalScene.playSound("se/flee");
@ -65,7 +60,10 @@ export class AttemptRunPhase extends FieldPhase {
globalScene.phaseManager.pushNew("NewBattlePhase"); globalScene.phaseManager.pushNew("NewBattlePhase");
} else { } else {
globalScene.currentBattle.failedRunAway = true; activePlayerField.forEach(p => {
p.turnData.failedRunAway = true;
});
globalScene.phaseManager.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500); globalScene.phaseManager.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500);
} }

View File

@ -3,19 +3,6 @@ import { TrainerSlot } from "#enums/trainer-slot";
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";
export abstract class BattlePhase extends Phase { export abstract class BattlePhase extends Phase {
start() {
if (globalScene.currentBattle.failedRunAway) {
const activePlayerField = globalScene.getPlayerField(true);
activePlayerField.forEach(p => {
p.turnData.failedRunAway = true;
});
//Reset flag for future run attempts
globalScene.currentBattle.failedRunAway = false;
}
}
showEnemyTrainer(trainerSlot: TrainerSlot = TrainerSlot.NONE): void { showEnemyTrainer(trainerSlot: TrainerSlot = TrainerSlot.NONE): void {
if (!globalScene.currentBattle.trainer) { if (!globalScene.currentBattle.trainer) {
console.warn("Enemy trainer is missing!"); console.warn("Enemy trainer is missing!");

View File

@ -208,7 +208,7 @@ export class TurnStartPhase extends FieldPhase {
break; break;
case Command.RUN: case Command.RUN:
{ {
//Team Based action, no need for checking individual pokemon or whether its doubles // Running (like ball throwing) is a team action taking up both Pokemon's turns.
phaseManager.unshiftNew("AttemptRunPhase"); phaseManager.unshiftNew("AttemptRunPhase");
} }
break; break;

View File

@ -1,32 +1,20 @@
import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon"; import { globalScene } from "#app/global-scene";
import type { NumberHolder } from "#app/utils/common";
import type Pokemon from "#app/field/pokemon";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import Overrides from "#app/overrides";
/** /**
* Calculates the chance for the player's team to successfully run away from battle. * Calculate the chance for the player's team to successfully run away from battle.
* *
* @param playerField The player's currently active Pokémon * @param escapeAttempts The number of prior failed escape attempts in the current battle
* @param enemyField The enemy team's Pokémon * @returns The final escape chance, as percentage out of 100.
* @param escapeChance A mutable NumberHolder to be assigned the final escape chance value
* @param escapeAttempts The number of previous escape attempts made in the battle
*/ */
export function calculateEscapeChance( export function calculateEscapeChance(escapeAttempts: number): number {
playerField: PlayerPokemon[], const enemyField = globalScene.getEnemyField();
enemyField: EnemyPokemon[], const activePlayerField = globalScene.getPlayerField(true);
escapeChance: NumberHolder,
escapeAttempts: number, // Cf https://bulbapedia.bulbagarden.net/wiki/Escape#Generation_V_onwards
) { // From gen 5 onwards, running takes the _base_ speed totals of both party sides.
/** Sum of the speed of all enemy pokemon on the field */ const enemySpeed = enemyField.reduce((total, enemy) => total + enemy.getStat(Stat.SPD), 0);
const enemySpeed = enemyField.reduce( const playerSpeed = activePlayerField.reduce((total, player) => total + player.getStat(Stat.SPD), 0);
(total: number, enemyPokemon: Pokemon) => total + enemyPokemon.getStat(Stat.SPD),
0,
);
/** Sum of the speed of all player pokemon on the field */
const playerSpeed = playerField.reduce(
(total: number, playerPokemon: Pokemon) => total + playerPokemon.getStat(Stat.SPD),
0,
);
/* The way the escape chance works is by looking at the difference between your speed and the enemy field's average speed as a ratio. The higher this ratio, the higher your chance of success. /* The way the escape chance works is by looking at the difference between your speed and the enemy field's average speed as a ratio. The higher this ratio, the higher your chance of success.
* However, there is a cap for the ratio of your speed vs enemy speed which beyond that point, you won't gain any advantage. It also looks at how many times you've tried to escape. * However, there is a cap for the ratio of your speed vs enemy speed which beyond that point, you won't gain any advantage. It also looks at how many times you've tried to escape.
@ -44,10 +32,8 @@ export function calculateEscapeChance(
* From the above, we can calculate the below values * From the above, we can calculate the below values
*/ */
let isBoss = false; /** Whether at least 1 pokemon on the enemy field is a boss. */
for (let e = 0; e < enemyField.length; e++) { const isBoss = enemyField.some(e => e.isBoss());
isBoss = isBoss || enemyField[e].isBoss(); // this line checks if any of the enemy pokemon on the field are bosses; if so, the calculation for escaping is different
}
/** The ratio between the speed of your active pokemon and the speed of the enemy field */ /** The ratio between the speed of your active pokemon and the speed of the enemy field */
const speedRatio = playerSpeed / enemySpeed; const speedRatio = playerSpeed / enemySpeed;
@ -62,8 +48,13 @@ export function calculateEscapeChance(
/** Slope of the escape chance curve */ /** Slope of the escape chance curve */
const escapeSlope = (maxChance - minChance) / speedCap; const escapeSlope = (maxChance - minChance) / speedCap;
// Check for override, guaranteeing or forbidding random flee attempts as applicable.
if (Overrides.RUN_SUCCESS_OVERRIDE !== null) {
return Overrides.RUN_SUCCESS_OVERRIDE ? 100 : 0;
}
// This will calculate the escape chance given all of the above and clamp it to the range of [`minChance`, `maxChance`] // This will calculate the escape chance given all of the above and clamp it to the range of [`minChance`, `maxChance`]
escapeChance.value = Phaser.Math.Clamp( return Phaser.Math.Clamp(
Math.round(escapeSlope * speedRatio + minChance + escapeBonus * escapeAttempts), Math.round(escapeSlope * speedRatio + minChance + escapeBonus * escapeAttempts),
minChance, minChance,
maxChance, maxChance,

View File

@ -6,6 +6,7 @@ import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Overrides from "#app/overrides";
describe("Abilities - Honey Gather", () => { describe("Abilities - Honey Gather", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -63,6 +64,8 @@ describe("Abilities - Honey Gather", () => {
// something weird is going on with the test framework, so this is required to prevent a crash // something weird is going on with the test framework, so this is required to prevent a crash
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "scene", "get").mockReturnValue(game.scene); vi.spyOn(enemy, "scene", "get").mockReturnValue(game.scene);
//Expects next wave so run must succeed
vi.spyOn(Overrides, "RUN_SUCCESS_OVERRIDE", "get").mockReturnValue(true);
const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase;
commandPhase.handleCommand(Command.RUN, 0); commandPhase.handleCommand(Command.RUN, 0);

View File

@ -4,10 +4,11 @@ import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { CommandPhase } from "#app/phases/command-phase"; import type { CommandPhase } from "#app/phases/command-phase";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
import Overrides from "#app/overrides";
describe("Abilities - Speed Boost", () => { describe("Abilities - Speed Boost", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -96,12 +97,15 @@ describe("Abilities - Speed Boost", () => {
}); });
it("should not trigger if pokemon fails to escape", async () => { it("should not trigger if pokemon fails to escape", async () => {
//Account for doubles, should not trigger on either pokemon
game.override.battleStyle("double");
await game.classicMode.startBattle([SpeciesId.SHUCKLE]); await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
vi.spyOn(Overrides, "RUN_SUCCESS_OVERRIDE", "get").mockReturnValue(false);
const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase;
commandPhase.handleCommand(Command.RUN, 0); commandPhase.handleCommand(Command.RUN, 0);
const runPhase = game.scene.phaseManager.getCurrentPhase() as AttemptRunPhase;
runPhase.forceFailEscape = true;
await game.phaseInterceptor.to(AttemptRunPhase); await game.phaseInterceptor.to(AttemptRunPhase);
await game.toNextTurn(); await game.toNextTurn();

View File

@ -1,7 +1,6 @@
import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
import type { CommandPhase } from "#app/phases/command-phase"; import type { CommandPhase } from "#app/phases/command-phase";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { NumberHolder } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
@ -45,7 +44,6 @@ describe("Escape chance calculations", () => {
commandPhase.handleCommand(Command.RUN, 0); commandPhase.handleCommand(Command.RUN, 0);
await game.phaseInterceptor.to(AttemptRunPhase, false); await game.phaseInterceptor.to(AttemptRunPhase, false);
const escapePercentage = new NumberHolder(0);
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping // this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
const escapeChances: { const escapeChances: {
@ -91,8 +89,8 @@ describe("Escape chance calculations", () => {
20, 20,
escapeChances[i].pokemonSpeedRatio * enemySpeed, escapeChances[i].pokemonSpeedRatio * enemySpeed,
]); ]);
calculateEscapeChance(playerPokemon, enemyField, escapePercentage, game.scene.currentBattle.escapeAttempts); const chance = calculateEscapeChance(game.scene.currentBattle.escapeAttempts);
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance); expect(chance).toBe(escapeChances[i].expectedEscapeChance);
} }
}); });
@ -117,7 +115,6 @@ describe("Escape chance calculations", () => {
commandPhase.handleCommand(Command.RUN, 0); commandPhase.handleCommand(Command.RUN, 0);
await game.phaseInterceptor.to(AttemptRunPhase, false); await game.phaseInterceptor.to(AttemptRunPhase, false);
const escapePercentage = new NumberHolder(0);
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping // this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
const escapeChances: { const escapeChances: {
@ -171,9 +168,9 @@ describe("Escape chance calculations", () => {
20, 20,
escapeChances[i].pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5], escapeChances[i].pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5],
]); ]);
calculateEscapeChance(playerPokemon, enemyField, escapePercentage, game.scene.currentBattle.escapeAttempts); const chance = calculateEscapeChance(game.scene.currentBattle.escapeAttempts);
// checks to make sure the escape values are the same // checks to make sure the escape values are the same
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance); expect(chance).toBe(escapeChances[i].expectedEscapeChance);
// checks to make sure the sum of the player's speed for all pokemon is equal to the appropriate ratio of the total enemy speed // checks to make sure the sum of the player's speed for all pokemon is equal to the appropriate ratio of the total enemy speed
expect(playerPokemon[0].stats[5] + playerPokemon[1].stats[5]).toBe( expect(playerPokemon[0].stats[5] + playerPokemon[1].stats[5]).toBe(
escapeChances[i].pokemonSpeedRatio * totalEnemySpeed, escapeChances[i].pokemonSpeedRatio * totalEnemySpeed,
@ -195,7 +192,6 @@ describe("Escape chance calculations", () => {
commandPhase.handleCommand(Command.RUN, 0); commandPhase.handleCommand(Command.RUN, 0);
await game.phaseInterceptor.to(AttemptRunPhase, false); await game.phaseInterceptor.to(AttemptRunPhase, false);
const escapePercentage = new NumberHolder(0);
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping // this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
const escapeChances: { const escapeChances: {
@ -254,8 +250,8 @@ describe("Escape chance calculations", () => {
20, 20,
escapeChances[i].pokemonSpeedRatio * enemySpeed, escapeChances[i].pokemonSpeedRatio * enemySpeed,
]); ]);
calculateEscapeChance(playerPokemon, enemyField, escapePercentage, game.scene.currentBattle.escapeAttempts); const chance = calculateEscapeChance(game.scene.currentBattle.escapeAttempts);
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance); expect(chance).toBe(escapeChances[i].expectedEscapeChance);
} }
}); });
@ -280,7 +276,6 @@ describe("Escape chance calculations", () => {
commandPhase.handleCommand(Command.RUN, 0); commandPhase.handleCommand(Command.RUN, 0);
await game.phaseInterceptor.to(AttemptRunPhase, false); await game.phaseInterceptor.to(AttemptRunPhase, false);
const escapePercentage = new NumberHolder(0);
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping // this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
const escapeChances: { const escapeChances: {
@ -346,9 +341,9 @@ describe("Escape chance calculations", () => {
20, 20,
escapeChances[i].pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5], escapeChances[i].pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5],
]); ]);
calculateEscapeChance(playerPokemon, enemyField, escapePercentage, game.scene.currentBattle.escapeAttempts); const chance = calculateEscapeChance(game.scene.currentBattle.escapeAttempts);
// checks to make sure the escape values are the same // checks to make sure the escape values are the same
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance); expect(chance).toBe(escapeChances[i].expectedEscapeChance);
// checks to make sure the sum of the player's speed for all pokemon is equal to the appropriate ratio of the total enemy speed // checks to make sure the sum of the player's speed for all pokemon is equal to the appropriate ratio of the total enemy speed
expect(playerPokemon[0].stats[5] + playerPokemon[1].stats[5]).toBe( expect(playerPokemon[0].stats[5] + playerPokemon[1].stats[5]).toBe(
escapeChances[i].pokemonSpeedRatio * totalEnemySpeed, escapeChances[i].pokemonSpeedRatio * totalEnemySpeed,