diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 55f66728450..fbc13f58bb0 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2230,7 +2230,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage); } - console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); + // debug message for when damage is applied (i.e. not simulated) + if (!simulated) { + console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); + } let hitResult: HitResult; if (typeMultiplier < 1) { @@ -4027,9 +4030,9 @@ export class EnemyPokemon extends Pokemon { return false; } const move = pkmnMove.getMove()!; - const activePokemon = this.scene.getField(true); + const fieldPokemon = this.scene.getField(); const moveTargets = getMoveTargets(this, move.id).targets - .map(ind => activePokemon[ind]) + .map(ind => fieldPokemon[ind]) .filter(p => this.isPlayer() !== p.isPlayer()); // Only considers critical hits for crit-only moves or when this Pokemon is under the effect of Laser Focus const isCritical = move.hasAttr(CritOnlyAttr) || !!this.getTag(BattlerTagType.ALWAYS_CRIT); diff --git a/src/test/enemy_command.test.ts b/src/test/enemy_command.test.ts new file mode 100644 index 00000000000..39b77845ea9 --- /dev/null +++ b/src/test/enemy_command.test.ts @@ -0,0 +1,75 @@ +import { allMoves, MoveCategory } from "#app/data/move"; +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import { AiType, EnemyPokemon } from "#app/field/pokemon"; +import { randSeedInt } from "#app/utils"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; + +const TIMEOUT = 20 * 1000; +const NUM_TRIALS = 300; + +type MoveChoiceSet = { [key: number]: number }; + +function getEnemyMoveChoices(pokemon: EnemyPokemon, moveChoices: MoveChoiceSet): void { + // Use an unseeded random number generator in place of the mocked-out randBattleSeedInt + vi.spyOn(pokemon.scene, "randBattleSeedInt").mockImplementation((range, min?) => { + return randSeedInt(range, min); + }); + for (let i = 0; i < NUM_TRIALS; i++) { + const queuedMove = pokemon.getNextMove(); + moveChoices[queuedMove.move]++; + } + + for (const [moveId, count] of Object.entries(moveChoices)) { + console.log(`Move: ${allMoves[moveId].name} Count: ${count} (${count / NUM_TRIALS * 100}%)`); + } +} + +describe("Enemy Commands - Move Selection", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + game = new GameManager(phaserGame); + game.override.ability(Abilities.BALL_FETCH); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + it( + "should never use Status moves if an attack can KO", + async () => { + game.override + .enemySpecies(Species.ETERNATUS) + .enemyMoveset([Moves.ETERNABEAM, Moves.SLUDGE_BOMB, Moves.DRAGON_DANCE, Moves.COSMIC_POWER]) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.BALL_FETCH) + .startingLevel(1) + .enemyLevel(100); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.aiType = AiType.SMART_RANDOM; + + const moveChoices: MoveChoiceSet = {}; + const enemyMoveset = enemyPokemon.getMoveset(); + enemyMoveset.forEach(mv => moveChoices[mv!.moveId] = 0); + getEnemyMoveChoices(enemyPokemon, moveChoices); + + enemyMoveset.forEach(mv => { + if (mv?.getMove().category === MoveCategory.STATUS) { + expect(moveChoices[mv.moveId]).toBe(0); + } + }); + }, TIMEOUT + ); +});