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 * Calculates and retrieves the final value of a stat considering any held
* items, move effects, opponent abilities, and whether there was a critical * 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) { if (critOnly.value || critAlways) {
isCritical = true; isCritical = true;
} else { } else {
const critStage = new Utils.IntegerHolder(0); const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))];
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))];
isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance);
if (Overrides.NEVER_CRIT_OVERRIDE) { if (Overrides.NEVER_CRIT_OVERRIDE) {
isCritical = false; 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 { TurnEndPhase } from "#app/phases/turn-end-phase.js";
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 * as Utils from "#app/utils";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -26,91 +23,64 @@ describe("Items - Leek", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.enemySpecies(Species.MAGIKARP); game.override
game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); .enemySpecies(Species.MAGIKARP)
game.override.disableCrits(); .enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH])
.startingHeldItems([{ name: "LEEK" }])
game.override.battleType("single"); .moveset([ Moves.TACKLE ])
.disableCrits()
.battleType("single");
}); });
it("LEEK activates in battle correctly", async () => { it("should raise CRIT stage by 2 when held by FARFETCHD", async () => {
game.override.startingHeldItems([{ name: "LEEK" }]);
game.override.moveset([Moves.POUND]);
const consoleSpy = vi.spyOn(console, "log");
await game.startBattle([ await game.startBattle([
Species.FARFETCHD 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); }, 20000);
it("LEEK held by FARFETCHD", async () => { it("should raise CRIT stage by 2 when held by GALAR_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 () => {
await game.startBattle([ await game.startBattle([
Species.GALAR_FARFETCHD Species.GALAR_FARFETCHD
]); ]);
const partyMember = game.scene.getPlayerPokemon()!; const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item vi.spyOn(enemyMember, "getCritStage");
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(0); game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies await game.phaseInterceptor.to(TurnEndPhase);
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(2); expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 20000);
it("LEEK held by SIRFETCHD", async () => { it("should raise CRIT stage by 2 when held by SIRFETCHD", async () => {
await game.startBattle([ await game.startBattle([
Species.SIRFETCHD Species.SIRFETCHD
]); ]);
const partyMember = game.scene.getPlayerPokemon()!; const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item vi.spyOn(enemyMember, "getCritStage");
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(0); game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies await game.phaseInterceptor.to(TurnEndPhase);
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(2); expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 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 // Randomly choose from the Farfetch'd line
const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD]; const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD];
@ -119,9 +89,7 @@ describe("Items - Leek", () => {
Species.PIKACHU, Species.PIKACHU,
]); ]);
const party = game.scene.getParty(); const [ partyMember, ally ] = game.scene.getParty();
const partyMember = party[0];
const ally = party[1];
// Fuse party members (taken from PlayerPokemon.fuse(...) function) // Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species; partyMember.fusionSpecies = ally.species;
@ -132,20 +100,18 @@ describe("Items - Leek", () => {
partyMember.fusionGender = ally.gender; partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck; partyMember.fusionLuck = ally.luck;
// Making sure modifier is not applied without holding item const enemyMember = game.scene.getEnemyPokemon()!;
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(0); vi.spyOn(enemyMember, "getCritStage");
// Giving Leek to party member and testing if it applies game.move.select(Moves.TACKLE);
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(2); await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 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 // Randomly choose from the Farfetch'd line
const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD]; const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD];
@ -154,9 +120,7 @@ describe("Items - Leek", () => {
species[Utils.randInt(species.length)] species[Utils.randInt(species.length)]
]); ]);
const party = game.scene.getParty(); const [ partyMember, ally ] = game.scene.getParty();
const partyMember = party[0];
const ally = party[1];
// Fuse party members (taken from PlayerPokemon.fuse(...) function) // Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species; partyMember.fusionSpecies = ally.species;
@ -167,36 +131,31 @@ describe("Items - Leek", () => {
partyMember.fusionGender = ally.gender; partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck; 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 vi.spyOn(enemyMember, "getCritStage");
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(2); game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 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([ await game.startBattle([
Species.PIKACHU Species.PIKACHU
]); ]);
const partyMember = game.scene.getPlayerPokemon()!; const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item vi.spyOn(enemyMember, "getCritStage");
const critStage = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(0); game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies await game.phaseInterceptor.to(TurnEndPhase);
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critStage);
expect(critStage.value).toBe(0); expect(enemyMember.getCritStage).toHaveReturnedWith(0);
}, 20000); }, 20000);
}); });

View File

@ -1,13 +1,10 @@
import { BattlerIndex } from "#app/battle"; import { TurnEndPhase } from "#app/phases/turn-end-phase.js";
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 { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phase from "phaser"; import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
describe("Items - Scope Lens", () => { describe("Items - Scope Lens", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -26,47 +23,29 @@ describe("Items - Scope Lens", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.enemySpecies(Species.MAGIKARP); game.override
game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); .enemySpecies(Species.MAGIKARP)
game.override.disableCrits(); .enemyMoveset(SPLASH_ONLY)
.moveset([ Moves.POUND ])
.startingHeldItems([{ name: "SCOPE_LENS" }])
.battleType("single")
.disableCrits();
game.override.battleType("single");
}, 20000); }, 20000);
it("SCOPE_LENS activates in battle correctly", async () => { it("should raise CRIT stage by 1", async () => {
game.override.startingHeldItems([{ name: "SCOPE_LENS" }]);
game.override.moveset([Moves.POUND]);
const consoleSpy = vi.spyOn(console, "log");
await game.startBattle([ await game.startBattle([
Species.GASTLY Species.GASTLY
]); ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "getCritStage");
game.move.select(Moves.POUND); game.move.select(Moves.POUND);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to(MoveEffectPhase); expect(enemyPokemon.getCritStage).toHaveReturnedWith(1);
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);
}, 20000); }, 20000);
}); });