Revamp Crit-Based Unit Tests & Dire Hit

This commit is contained in:
xsn34kzx 2024-08-23 00:12:10 -04:00
parent eeb1ff5760
commit f77bf5351e
4 changed files with 190 additions and 144 deletions

View File

@ -746,6 +746,32 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
/**
* Retrieves the critical-hit stage considering the move used and the Pokemon
* who used it.
* @param source the {@linkcode Pokemon} who using the move
* @param move the {@linkcode Move} being used
* @returns the final critical-hit stage value
*/
getCritStage(source: Pokemon, move: Move): number {
const critStage = new Utils.IntegerHolder(0);
applyMoveAttrs(HighCritAttr, source, this, move, critStage);
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
this.scene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
const bonusCrit = new Utils.BooleanHolder(false);
//@ts-ignore
if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
if (bonusCrit.value) {
critStage.value += 1;
}
}
if (source.getTag(BattlerTagType.CRIT_BOOST)) {
critStage.value += 2;
}
console.log(`crit stage: +${critStage.value}`);
return critStage.value;
}
/**
* Calculates and retrieves the final value of a stat considering any held
* items, move effects, opponent abilities, and whether there was a critical
@ -2128,22 +2154,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (critOnly.value || critAlways) {
isCritical = true;
} else {
const critStage = new Utils.IntegerHolder(0);
applyMoveAttrs(HighCritAttr, source, this, move, critStage);
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
this.scene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
const bonusCrit = new Utils.BooleanHolder(false);
//@ts-ignore
if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
if (bonusCrit.value) {
critStage.value += 1;
}
}
if (source.getTag(BattlerTagType.CRIT_BOOST)) {
critStage.value += 2;
}
console.log(`crit stage: +${critStage.value}`);
const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critStage.value, 3))];
const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))];
isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance);
if (Overrides.NEVER_CRIT_OVERRIDE) {
isCritical = false;

View File

@ -0,0 +1,97 @@
import { TurnEndPhase } from "#app/phases/turn-end-phase.js";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
import { BattleEndPhase } from "#app/phases/battle-end-phase.js";
import { TempCritBoosterModifier } from "#app/modifier/modifier.js";
import { Mode } from "#app/ui/ui.js";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler.js";
import { Button } from "#app/enums/buttons.js";
import { CommandPhase } from "#app/phases/command-phase.js";
import { NewBattlePhase } from "#app/phases/new-battle-phase.js";
import { TurnInitPhase } from "#app/phases/turn-init-phase.js";
describe("Items - Dire Hit", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phase.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(SPLASH_ONLY)
.moveset([ Moves.POUND ])
.startingHeldItems([{ name: "DIRE_HIT" }])
.battleType("single")
.disableCrits();
}, 20000);
it("should raise CRIT stage by 1", async () => {
await game.startBattle([
Species.GASTLY
]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "getCritStage");
game.move.select(Moves.POUND);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyPokemon.getCritStage).toHaveReturnedWith(1);
}, 20000);
it("should renew how many battles are left of existing DIRE_HIT when picking up new DIRE_HIT", async() => {
game.override.itemRewards([{ name: "DIRE_HIT" }]);
await game.startBattle([
Species.PIKACHU
]);
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.phaseInterceptor.to(BattleEndPhase);
const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier;
expect(modifier.getBattlesLeft()).toBe(4);
// Forced DIRE_HIT to spawn in the first slot with override
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
// Traverse to first modifier slot
handler.processInput(Button.LEFT);
handler.processInput(Button.UP);
handler.processInput(Button.ACTION);
}, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(NewBattlePhase), true);
await game.phaseInterceptor.to(TurnInitPhase);
// Making sure only one booster is in the modifier list even after picking up another
let count = 0;
for (const m of game.scene.modifiers) {
if (m instanceof TempCritBoosterModifier) {
count++;
expect((m as TempCritBoosterModifier).getBattlesLeft()).toBe(5);
}
}
expect(count).toBe(1);
}, 20000);
});

View File

@ -1,7 +1,4 @@
import { BattlerIndex } from "#app/battle";
import { CritBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase.js";
import * as Utils from "#app/utils";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@ -26,91 +23,64 @@ describe("Items - Leek", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.enemySpecies(Species.MAGIKARP);
game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
game.override.disableCrits();
game.override.battleType("single");
game.override
.enemySpecies(Species.MAGIKARP)
.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH])
.startingHeldItems([{ name: "LEEK" }])
.moveset([ Moves.TACKLE ])
.disableCrits()
.battleType("single");
});
it("LEEK activates in battle correctly", async () => {
game.override.startingHeldItems([{ name: "LEEK" }]);
game.override.moveset([Moves.POUND]);
const consoleSpy = vi.spyOn(console, "log");
it("should raise CRIT stage by 2 when held by FARFETCHD", async () => {
await game.startBattle([
Species.FARFETCHD
]);
game.move.select(Moves.POUND);
const enemyMember = game.scene.getEnemyPokemon()!;
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
vi.spyOn(enemyMember, "getCritStage");
await game.phaseInterceptor.to(MoveEffectPhase);
game.move.select(Moves.TACKLE);
expect(consoleSpy).toHaveBeenCalledWith("Applied", "Leek", "");
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
it("LEEK held by FARFETCHD", async () => {
await game.startBattle([
Species.FARFETCHD
]);
const partyMember = game.scene.getPlayerPokemon()!;
// Making sure modifier is not applied without holding item
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(0);
// Giving Leek to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(2);
}, 20000);
it("LEEK held by GALAR_FARFETCHD", async () => {
it("should raise CRIT stage by 2 when held by GALAR_FARFETCHD", async () => {
await game.startBattle([
Species.GALAR_FARFETCHD
]);
const partyMember = game.scene.getPlayerPokemon()!;
const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
vi.spyOn(enemyMember, "getCritStage");
expect(critStage.value).toBe(0);
game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
await game.phaseInterceptor.to(TurnEndPhase);
expect(critStage.value).toBe(2);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
it("LEEK held by SIRFETCHD", async () => {
it("should raise CRIT stage by 2 when held by SIRFETCHD", async () => {
await game.startBattle([
Species.SIRFETCHD
]);
const partyMember = game.scene.getPlayerPokemon()!;
const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
vi.spyOn(enemyMember, "getCritStage");
expect(critStage.value).toBe(0);
game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
await game.phaseInterceptor.to(TurnEndPhase);
expect(critStage.value).toBe(2);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
it("LEEK held by fused FARFETCHD line (base)", async () => {
it("should raise CRIT stage by 2 when held by FARFETCHD line fused with Pokemon", async () => {
// Randomly choose from the Farfetch'd line
const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD];
@ -119,9 +89,7 @@ describe("Items - Leek", () => {
Species.PIKACHU,
]);
const party = game.scene.getParty();
const partyMember = party[0];
const ally = party[1];
const [ partyMember, ally ] = game.scene.getParty();
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species;
@ -132,20 +100,18 @@ describe("Items - Leek", () => {
partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck;
// Making sure modifier is not applied without holding item
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
const enemyMember = game.scene.getEnemyPokemon()!;
expect(critStage.value).toBe(0);
vi.spyOn(enemyMember, "getCritStage");
// Giving Leek to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
game.move.select(Moves.TACKLE);
expect(critStage.value).toBe(2);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
it("LEEK held by fused FARFETCHD line (part)", async () => {
it("should raise CRIT stage by 2 when held by Pokemon fused with FARFETCHD line", async () => {
// Randomly choose from the Farfetch'd line
const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD];
@ -154,9 +120,7 @@ describe("Items - Leek", () => {
species[Utils.randInt(species.length)]
]);
const party = game.scene.getParty();
const partyMember = party[0];
const ally = party[1];
const [ partyMember, ally ] = game.scene.getParty();
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species;
@ -167,36 +131,31 @@ describe("Items - Leek", () => {
partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck;
// Making sure modifier is not applied without holding item
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(0);
const enemyMember = game.scene.getEnemyPokemon()!;
// Giving Leek to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
vi.spyOn(enemyMember, "getCritStage");
expect(critStage.value).toBe(2);
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000);
it("LEEK not held by FARFETCHD line", async () => {
it("should not raise CRIT stage when held by a Pokemon outside of FARFETCHD line", async () => {
await game.startBattle([
Species.PIKACHU
]);
const partyMember = game.scene.getPlayerPokemon()!;
const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
vi.spyOn(enemyMember, "getCritStage");
expect(critStage.value).toBe(0);
game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
await game.phaseInterceptor.to(TurnEndPhase);
expect(critStage.value).toBe(0);
expect(enemyMember.getCritStage).toHaveReturnedWith(0);
}, 20000);
});

View File

@ -1,13 +1,10 @@
import { BattlerIndex } from "#app/battle";
import { CritBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { TurnEndPhase } from "#app/phases/turn-end-phase.js";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
describe("Items - Scope Lens", () => {
let phaserGame: Phaser.Game;
@ -26,47 +23,29 @@ describe("Items - Scope Lens", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.enemySpecies(Species.MAGIKARP);
game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
game.override.disableCrits();
game.override
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(SPLASH_ONLY)
.moveset([ Moves.POUND ])
.startingHeldItems([{ name: "SCOPE_LENS" }])
.battleType("single")
.disableCrits();
game.override.battleType("single");
}, 20000);
it("SCOPE_LENS activates in battle correctly", async () => {
game.override.startingHeldItems([{ name: "SCOPE_LENS" }]);
game.override.moveset([Moves.POUND]);
const consoleSpy = vi.spyOn(console, "log");
it("should raise CRIT stage by 1", async () => {
await game.startBattle([
Species.GASTLY
]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "getCritStage");
game.move.select(Moves.POUND);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to(MoveEffectPhase);
expect(consoleSpy).toHaveBeenCalledWith("Applied", "Scope Lens", "");
}, 20000);
it("SCOPE_LENS held by random pokemon", async () => {
await game.startBattle([
Species.GASTLY
]);
const partyMember = game.scene.getPlayerPokemon()!;
// Making sure modifier is not applied without holding item
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(0);
// Giving Scope Lens to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.SCOPE_LENS().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(1);
expect(enemyPokemon.getCritStage).toHaveReturnedWith(1);
}, 20000);
});