Compare commits

...

11 Commits

Author SHA1 Message Date
Bertie690
3f31f0e13e
Merge b393cf8914 into 1ff2701964 2025-06-19 22:29:37 -04:00
lnuvy
1ff2701964
[Bug] Fix when using arrow keys in Pokédex after catching a Pokémon from mystery event (#6000)
fix: wrap setOverlayMode args in array to mystery-encounter

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-06-19 20:45:54 -04:00
Bertie690
1e306e25b5
[Move] Fixed Chilly Reception displaying message when used virtually
https://github.com/pagefaultgames/pokerogue/pull/5843

* Fixed Chilly Reception displaying message when used virtually

* Fixed lack of message causing Chilly Reception to fail

* Fixed tests

* Reverted bool change + fixed test

* Fixed test
2025-06-19 17:14:05 -07:00
Bertie690
b393cf8914
Merge branch 'beta' into test-cleanup 2025-06-19 20:05:44 -04:00
Madmadness65
43aa772603
[UI/UX] Add Pokémon category flavor text to Pokédex (#5957)
* Add Pokémon category flavor text to Pokédex

* Append `_category` to locale entry
2025-06-20 00:04:57 +00:00
Bertie690
f231755db2 fixed using sizzly slide instead of will o wisp to inflict burn 2025-06-17 18:27:27 -04:00
Bertie690
6d777bb7c8 Fixed innards out 2025-06-17 17:14:44 -04:00
Bertie690
fcd270ed9b fixed tests 2025-06-17 17:08:43 -04:00
Bertie690
86a3f6d711 Merge remote-tracking branch 'upstream/beta' into test-cleanup 2025-06-17 17:03:40 -04:00
Bertie690
12a626d2a5 Fixde magic guard aftermath 2025-06-15 10:25:49 -04:00
Bertie690
be3c42a531 Fixed tests for Magic Guard, Battle phase; fixed innards out stuff 2025-06-13 13:45:54 -04:00
12 changed files with 376 additions and 504 deletions

View File

@ -6542,13 +6542,26 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
_hitResult?: HitResult,
..._args: any[]
): boolean {
const diedToDirectDamage =
move !== undefined &&
attacker !== undefined &&
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon });
// Return early if ability user did not die to a direct-contact attack.
if (
move === undefined ||
attacker === undefined ||
!move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon })
) {
return false;
}
const cancelled = new BooleanHolder(false);
globalScene.getField(true).map(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled, simulated));
return !(!diedToDirectDamage || cancelled.value || attacker!.hasAbilityWithAttr("BlockNonDirectDamageAbAttr"));
// TODO: This should be in speed order
globalScene.getField(true).forEach(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled, simulated));
if (cancelled.value) {
return false;
}
// TODO: Does aftermath display text if the attacker has Magic Guard?
applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled);
return !cancelled.value;
}
override applyPostFaint(
@ -6557,15 +6570,15 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
simulated: boolean,
attacker?: Pokemon,
_move?: Move,
_hitResult?: HitResult,
..._args: any[]
): void {
if (!simulated) {
attacker!.damageAndUpdate(toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), {
result: HitResult.INDIRECT,
});
attacker!.turnData.damageTaken += toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio));
if (simulated) {
return;
}
attacker!.damageAndUpdate(toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), {
result: HitResult.INDIRECT,
});
attacker!.turnData.damageTaken += toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio));
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
@ -6577,27 +6590,38 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
}
/**
* Attribute used for abilities (Innards Out) that damage the opponent based on how much HP the last attack used to knock out the owner of the ability.
* Attribute used for abilities that damage an opponent who faints the ability holder
* equal to the amount of damage the last attack inflicted.
*
* Used for {@linkcode Abilities.INNARDS_OUT}.
*/
export class PostFaintHPDamageAbAttr extends PostFaintAbAttr {
override applyPostFaint(
pokemon: Pokemon,
_passive: boolean,
simulated: boolean,
_simulated: boolean,
attacker?: Pokemon,
move?: Move,
_hitResult?: HitResult,
..._args: any[]
): void {
//If the mon didn't die to indirect damage
if (move !== undefined && attacker !== undefined && !simulated) {
const damage = pokemon.turnData.attacksReceived[0].damage;
attacker.damageAndUpdate(damage, { result: HitResult.INDIRECT });
attacker.turnData.damageTaken += damage;
// return early if the user died to indirect damage, target has magic guard or was KO'd by an ally
if (move === undefined || attacker === undefined || attacker.getAlly() === pokemon) {
return;
}
// TODO: Confirm that magic guard's flyout shows here?
const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled);
if (cancelled.value) {
return;
}
const damage = pokemon.turnData.attacksReceived[0].damage;
attacker.damageAndUpdate(damage, { result: HitResult.INDIRECT });
attacker.turnData.damageTaken += damage;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
// Oddly, Innards Out still shows a flyout if the effect was blocked due to Magic Guard...
override getTriggerMessage(pokemon: Pokemon, abilityName: string): string {
return i18next.t("abilityTriggers:postFaintHpDamage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName,

View File

@ -93,6 +93,10 @@ import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap
import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils";
/**
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
* Conventionally returns `true` for success and `false` for failure.
*/
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
@ -1390,18 +1394,31 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
}
}
/**
* Attribute to display a message before a move is executed.
*/
export class PreMoveMessageAttr extends MoveAttr {
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
/** The message to display or a function returning one */
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined);
/**
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
* @param message - The message to display before move use, either as a string or a function producing one.
* @remarks
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed
* (though the move will still succeed).
*/
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) {
super();
this.message = message;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const message = typeof this.message === "string"
? this.message as string
: this.message(user, target, move);
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
const message = typeof this.message === "function"
? this.message(user, target, move)
: this.message;
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
if (message) {
globalScene.phaseManager.queueMessage(message, 500);
return true;
@ -11299,7 +11316,11 @@ export function initMoves() {
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
.condition(failIfLastInPartyCondition),
new SelfStatusMove(MoveId.CHILLY_RECEPTION, PokemonType.ICE, -1, 10, -1, 0, 9)
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(PreMoveMessageAttr, (user, _target, _move) =>
// Don't display text if current move phase is follow up (ie move called indirectly)
isVirtual((globalScene.phaseManager.getCurrentPhase() as MovePhase).useMode)
? ""
: i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(ChillyReceptionAttr, true),
new SelfStatusMove(MoveId.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)

View File

@ -751,7 +751,7 @@ export async function catchPokemon(
UiMode.POKEDEX_PAGE,
pokemon.species,
pokemon.formIndex,
attributes,
[attributes],
null,
() => {
globalScene.ui.setMode(UiMode.MESSAGE).then(() => {

View File

@ -764,7 +764,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
readonly subLegendary: boolean;
readonly legendary: boolean;
readonly mythical: boolean;
readonly species: string;
public category: string;
readonly growthRate: GrowthRate;
/** The chance (as a decimal) for this Species to be male, or `null` for genderless species */
readonly malePercent: number | null;
@ -778,7 +778,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
subLegendary: boolean,
legendary: boolean,
mythical: boolean,
species: string,
category: string,
type1: PokemonType,
type2: PokemonType | null,
height: number,
@ -829,7 +829,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
this.subLegendary = subLegendary;
this.legendary = legendary;
this.mythical = mythical;
this.species = species;
this.category = category;
this.growthRate = growthRate;
this.malePercent = malePercent;
this.genderDiffs = genderDiffs;
@ -968,6 +968,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
localize(): void {
this.name = i18next.t(`pokemon:${SpeciesId[this.speciesId].toLowerCase()}`);
this.category = i18next.t(`pokemonCategory:${SpeciesId[this.speciesId].toLowerCase()}_category`);
}
getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): SpeciesId {

View File

@ -668,6 +668,9 @@ export class MovePhase extends BattlePhase {
}),
500,
);
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
// TODO: This assumes single target for message funcs - is this sustainable?
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
}

View File

@ -245,6 +245,7 @@ export async function initI18n(): Promise<void> {
"pokeball",
"pokedexUiHandler",
"pokemon",
"pokemonCategory",
"pokemonEvolutions",
"pokemonForm",
"pokemonInfo",

View File

@ -174,6 +174,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
private pokemonCaughtHatchedContainer: Phaser.GameObjects.Container;
private pokemonCaughtCountText: Phaser.GameObjects.Text;
private pokemonFormText: Phaser.GameObjects.Text;
private pokemonCategoryText: Phaser.GameObjects.Text;
private pokemonHatchedIcon: Phaser.GameObjects.Sprite;
private pokemonHatchedCountText: Phaser.GameObjects.Text;
private pokemonShinyIcons: Phaser.GameObjects.Sprite[];
@ -409,6 +410,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonFormText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonFormText);
this.pokemonCategoryText = addTextObject(100, 18, "Category", TextStyle.WINDOW_ALT, {
fontSize: "42px",
});
this.pokemonCategoryText.setOrigin(1, 0);
this.starterSelectContainer.add(this.pokemonCategoryText);
this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25);
this.pokemonCaughtHatchedContainer.setScale(0.5);
this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer);
@ -2354,6 +2361,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(true);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.pokemonCategoryText.setVisible(false);
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true);
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
@ -2382,6 +2390,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.pokemonCategoryText.setVisible(false);
this.setSpeciesDetails(species!, {
// TODO: is this bang correct?
@ -2534,6 +2543,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonNameText.setText(species ? "???" : "");
}
// Setting the category
if (isFormCaught) {
this.pokemonCategoryText.setText(species.category);
} else {
this.pokemonCategoryText.setText("");
}
// Setting tint of the sprite
if (isFormCaught) {
this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => {

View File

@ -0,0 +1,62 @@
import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Innards Out", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.ability(AbilityId.BALL_FETCH)
.battleStyle("single")
.criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.INNARDS_OUT)
.enemyMoveset(MoveId.SPLASH)
.startingLevel(100);
});
it("should damage opppnents that faint the ability holder for equal damage", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const magikarp = game.field.getEnemyPokemon();
magikarp.hp = 20;
game.move.use(MoveId.X_SCISSOR);
await game.toEndOfTurn();
expect(magikarp.isFainted()).toBe(true);
const feebas = game.field.getPlayerPokemon();
expect(feebas.getInverseHp()).toBe(20);
});
it("should not damage an ally in Double Battles", async () => {
game.override.battleStyle("double");
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const [magikarp1, magikarp2] = game.scene.getEnemyField();
magikarp1.hp = 1;
game.move.use(MoveId.PROTECT);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.SURF);
await game.toEndOfTurn();
expect(magikarp1.isFainted()).toBe(true);
expect(magikarp2.getInverseHp()).toBe(0);
});
});

View File

@ -1,19 +1,18 @@
import { getArenaTag } from "#app/data/arena-tag";
import { BattlerIndex } from "#enums/battler-index";
import { ArenaTagSide } from "#enums/arena-tag-side";
import { allMoves } from "#app/data/data-lists";
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { toDmgValue } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Magic Guard", () => {
describe("AbilityId - Magic Guard", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@ -29,404 +28,142 @@ describe("Abilities - Magic Guard", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
/** Player Pokemon overrides */
.ability(AbilityId.MAGIC_GUARD)
.moveset([MoveId.SPLASH])
.enemySpecies(SpeciesId.BLISSEY)
.enemyAbility(AbilityId.NO_GUARD)
.startingLevel(100)
/** Enemy Pokemon overrides */
.enemySpecies(SpeciesId.SNORLAX)
.enemyAbility(AbilityId.INSOMNIA)
.enemyMoveset(MoveId.SPLASH)
.enemyLevel(100);
});
//Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability)
it("ability should prevent damage caused by weather", async () => {
game.override.weather(WeatherType.SANDSTORM);
it.each<{ name: string; move?: MoveId; enemyMove?: MoveId }>([
{ name: "Non-Volatile Status Conditions", enemyMove: MoveId.TOXIC },
{ name: "Volatile Status Conditions", enemyMove: MoveId.LEECH_SEED },
{ name: "Crash Damage", move: MoveId.HIGH_JUMP_KICK },
{ name: "Variable Recoil Moves", move: MoveId.DOUBLE_EDGE },
{ name: "HP% Recoil Moves", move: MoveId.CHLOROBLAST },
])("should prevent damage from $name", async ({ move = MoveId.SPLASH, enemyMove = MoveId.SPLASH }) => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
// force a miss on HJK
vi.spyOn(allMoves[MoveId.HIGH_JUMP_KICK], "accuracy", "get").mockReturnValue(0);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.use(move);
await game.move.forceEnemyMove(enemyMove);
await game.toEndOfTurn();
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).toBeDefined();
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) has not taken damage from weather
* - The enemy Pokemon (without Magic Guard) has taken damage from weather
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
const magikarp = game.field.getPlayerPokemon();
expect(magikarp.hp).toBe(magikarp.getMaxHp());
});
it("ability should prevent damage caused by status effects but other non-damage effects still apply", async () => {
//Toxic keeps track of the turn counters -> important that Magic Guard keeps track of post-Toxic turns
game.override.statusEffect(StatusEffect.POISON);
it.each<{ abName: string; move?: MoveId; enemyMove?: MoveId; passive?: AbilityId; enemyAbility?: AbilityId }>([
{ abName: "Bad Dreams", enemyMove: MoveId.SPORE, enemyAbility: AbilityId.BAD_DREAMS },
{ abName: "Aftermath", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.AFTERMATH },
{ abName: "Innards Out", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.INNARDS_OUT },
{ abName: "Rough Skin", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.ROUGH_SKIN },
{ abName: "Dry Skin", move: MoveId.SUNNY_DAY, passive: AbilityId.DRY_SKIN },
{ abName: "Liquid Ooze", move: MoveId.DRAIN_PUNCH, enemyAbility: AbilityId.LIQUID_OOZE },
])(
"should prevent damage from $abName",
async ({
move = MoveId.SPLASH,
enemyMove = MoveId.SPLASH,
passive = AbilityId.BALL_FETCH,
enemyAbility = AbilityId.BALL_FETCH,
}) => {
game.override.enemyLevel(1).passiveAbility(passive).enemyAbility(enemyAbility);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.use(move);
await game.move.forceEnemyMove(enemyMove);
await game.toEndOfTurn();
const magikarp = game.field.getPlayerPokemon();
expect(magikarp.hp).toBe(magikarp.getMaxHp());
},
);
it.each<{ name: string; move?: MoveId; enemyMove?: MoveId }>([
{ name: "Struggle recoil", move: MoveId.STRUGGLE },
{ name: "Self-induced HP cutting", move: MoveId.BELLY_DRUM },
{ name: "Confusion self-damage", enemyMove: MoveId.CONFUSE_RAY },
])("should not prevent damage from $name", async ({ move = MoveId.SPLASH, enemyMove = MoveId.SPLASH }) => {
game.override.confusionActivation(true);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.use(move);
await game.move.forceEnemyMove(enemyMove);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); // For confuse ray
await game.toEndOfTurn();
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) has not taken damage from poison
* - The Pokemon's CatchRateMultiplier should be 1.5
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(getStatusEffectCatchRateMultiplier(leadPokemon.status!.effect)).toBe(1.5);
});
it("ability effect should not persist when the ability is replaced", async () => {
game.override.enemyMoveset(MoveId.WORRY_SEED).statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (that just lost its Magic Guard ability) has taken damage from poison
*/
const leadPokemon = game.field.getPlayerPokemon();
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage caused by burn but other non-damaging effects are still applied", async () => {
game.override.enemyStatusEffect(StatusEffect.BURN).enemyAbility(AbilityId.MAGIC_GUARD);
it("should preserve toxic turn count and deal appropriate damage when disabled", async () => {
game.override.statusEffect(StatusEffect.TOXIC);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPLASH);
game.move.use(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toNextTurn();
const enemyPokemon = game.scene.getEnemyPokemon()!;
const magikarp = game.field.getPlayerPokemon();
expect(magikarp.hp).toBe(magikarp.getMaxHp());
expect(magikarp.status?.toxicTurnCount).toBe(1);
await game.phaseInterceptor.to(TurnEndPhase);
// have a few turns pass
game.move.use(MoveId.SPLASH);
await game.toNextTurn();
game.move.use(MoveId.SPLASH);
await game.toNextTurn();
game.move.use(MoveId.SPLASH);
await game.toNextTurn();
expect(magikarp.status?.toxicTurnCount).toBe(4);
/**
* Expect:
* - The enemy Pokemon (with Magic Guard) has not taken damage from burn
* - The enemy Pokemon's physical attack damage is halved (TBD)
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5
*/
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
game.move.use(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.GASTRO_ACID);
await game.toNextTurn();
expect(magikarp.status?.toxicTurnCount).toBe(5);
expect(magikarp.getHpRatio(true)).toBeCloseTo(11 / 16, 1);
});
it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", async () => {
game.override.enemyStatusEffect(StatusEffect.TOXIC).enemyAbility(AbilityId.MAGIC_GUARD);
it("should preserve burn physical damage halving & status catch boost", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPLASH);
// NB: Burn applies directly to the physical dmg formula, so we can't just check attack here
game.move.use(MoveId.TACKLE);
await game.move.forceEnemyMove(MoveId.WILL_O_WISP);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
const enemyPokemon = game.scene.getEnemyPokemon()!;
const magikarp = game.field.getPlayerPokemon();
expect(magikarp.hp).toBe(magikarp.getMaxHp());
expect(magikarp.status?.effect).toBe(StatusEffect.BURN);
const toxicStartCounter = enemyPokemon.status!.toxicTurnCount;
//should be 0
const blissey = game.field.getEnemyPokemon();
const prevDmg = blissey.getInverseHp();
blissey.hp = blissey.getMaxHp();
await game.phaseInterceptor.to(TurnEndPhase);
expect(getStatusEffectCatchRateMultiplier(magikarp.status!.effect)).toBe(1.5);
/**
* Expect:
* - The enemy Pokemon (with Magic Guard) has not taken damage from toxic
* - The enemy Pokemon's status effect duration should be incremented
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5
*/
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(enemyPokemon.status!.toxicTurnCount).toBeGreaterThan(toxicStartCounter);
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
game.move.use(MoveId.TACKLE);
await game.toNextTurn();
const burntDmg = blissey.getInverseHp();
expect(burntDmg).toBeCloseTo(toDmgValue(prevDmg / 2), 0);
});
it("Magic Guard prevents damage caused by entry hazards", async () => {
//Adds and applies Spikes to both sides of the arena
const newTag = getArenaTag(ArenaTagType.SPIKES, 5, MoveId.SPIKES, 0, 0, ArenaTagSide.BOTH)!;
game.scene.arena.tags.push(newTag);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
const enemyPokemon = game.scene.getEnemyPokemon()!;
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) has not taken damage from spikes
* - The enemy Pokemon (without Magic Guard) has taken damage from spikes
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
});
it("Magic Guard does not prevent poison from Toxic Spikes", async () => {
//Adds and applies Spikes to both sides of the arena
const playerTag = getArenaTag(ArenaTagType.TOXIC_SPIKES, 5, MoveId.TOXIC_SPIKES, 0, 0, ArenaTagSide.PLAYER)!;
const enemyTag = getArenaTag(ArenaTagType.TOXIC_SPIKES, 5, MoveId.TOXIC_SPIKES, 0, 0, ArenaTagSide.ENEMY)!;
game.scene.arena.tags.push(playerTag);
game.scene.arena.tags.push(enemyTag);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
const enemyPokemon = game.scene.getEnemyPokemon()!;
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - Both Pokemon gain the poison status effect
* - The player Pokemon (with Magic Guard) has not taken damage from poison
* - The enemy Pokemon (without Magic Guard) has taken damage from poison
*/
expect(leadPokemon.status!.effect).toBe(StatusEffect.POISON);
expect(enemyPokemon.status!.effect).toBe(StatusEffect.POISON);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
});
it("Magic Guard prevents against damage from volatile status effects", async () => {
await game.classicMode.startBattle([SpeciesId.DUSKULL]);
game.override.moveset([MoveId.CURSE]).enemyAbility(AbilityId.MAGIC_GUARD);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.CURSE);
const enemyPokemon = game.scene.getEnemyPokemon()!;
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) has cut its HP to inflict curse
* - The enemy Pokemon (with Magic Guard) is cursed
* - The enemy Pokemon (with Magic Guard) does not lose HP from being cursed
*/
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
expect(enemyPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
});
it("Magic Guard prevents crash damage", async () => {
game.override.moveset([MoveId.HIGH_JUMP_KICK]);
it("should prevent damage from entry hazards, but not Toxic Spikes poison", async () => {
game.scene.arena.addTag(ArenaTagType.SPIKES, -1, MoveId.SPIKES, 0, ArenaTagSide.PLAYER);
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, -1, MoveId.TOXIC_SPIKES, 0, ArenaTagSide.PLAYER);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.HIGH_JUMP_KICK);
await game.move.forceMiss();
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) misses High Jump Kick but does not lose HP as a result
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage from recoil", async () => {
game.override.moveset([MoveId.TAKE_DOWN]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.TAKE_DOWN);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) uses a recoil move but does not lose HP from recoil
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard does not prevent damage from Struggle's recoil", async () => {
game.override.moveset([MoveId.STRUGGLE]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.STRUGGLE);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) uses Struggle but does lose HP from Struggle's recoil
*/
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
});
//This tests different move attributes than the recoil tests above
it("Magic Guard prevents self-damage from attacking moves", async () => {
game.override.moveset([MoveId.STEEL_BEAM]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.STEEL_BEAM);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) uses a move with an HP cost but does not lose HP from using it
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
/*
it("Magic Guard does not prevent self-damage from confusion", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.CHARM);
await game.phaseInterceptor.to(TurnEndPhase);
});
*/
it("Magic Guard does not prevent self-damage from non-attacking moves", async () => {
game.override.moveset([MoveId.BELLY_DRUM]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.BELLY_DRUM);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) uses a non-attacking move with an HP cost and thus loses HP from using it
*/
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage from abilities with PostTurnHurtIfSleepingAbAttr", async () => {
//Tests the ability Bad Dreams
game.override.statusEffect(StatusEffect.SLEEP);
//enemy pokemon is given Spore just in case player pokemon somehow awakens during test
game.override
.enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE])
.enemyAbility(AbilityId.BAD_DREAMS);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
* - The player Pokemon is asleep
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(leadPokemon.status!.effect).toBe(StatusEffect.SLEEP);
});
it("Magic Guard prevents damage from abilities with PostFaintContactDamageAbAttr", async () => {
//Tests the abilities Innards Out/Aftermath
game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.AFTERMATH);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
enemyPokemon.hp = 1;
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
* - The enemy Pokemon has fainted
*/
expect(enemyPokemon.hp).toBe(0);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage from abilities with PostDefendContactDamageAbAttr", async () => {
//Tests the abilities Iron Barbs/Rough Skin
game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.IRON_BARBS);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
* - The player Pokemon's move should have connected
*/
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage from abilities with ReverseDrainAbAttr", async () => {
//Tests the ability Liquid Ooze
game.override.moveset([MoveId.ABSORB]).enemyAbility(AbilityId.LIQUID_OOZE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(MoveId.ABSORB);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
* - The player Pokemon's move should have connected
*/
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard prevents HP loss from abilities with PostWeatherLapseDamageAbAttr", async () => {
game.override.passiveAbility(AbilityId.SOLAR_POWER).weather(WeatherType.SUNNY);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
// Magic guard prevented damage but not poison
const player = game.field.getPlayerPokemon();
expect(player.hp).toBe(player.getMaxHp());
expect(player.status?.effect).toBe(StatusEffect.POISON);
});
});

View File

@ -26,7 +26,7 @@ import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BiomeId } from "#enums/biome-id";
describe("Test Battle Phase", () => {
describe("Phase - Battle Phase", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@ -197,47 +197,25 @@ describe("Test Battle Phase", () => {
await game.phaseInterceptor.runFrom(SelectGenderPhase).to(SummonPhase);
});
it("2vs1", async () => {
game.override.battleStyle("single");
game.override.enemySpecies(SpeciesId.MIGHTYENA);
game.override.enemyAbility(AbilityId.HYDRATION);
game.override.ability(AbilityId.HYDRATION);
await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]);
expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND);
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name);
}, 20000);
it.each([
{ name: "1v1", double: false, qty: 1 },
{ name: "2v1", double: false, qty: 2 },
{ name: "2v2", double: true, qty: 2 },
{ name: "4v2", double: true, qty: 4 },
])("should not crash when starting $name battle", async ({ double, qty }) => {
game.override
.battleStyle(double ? "double" : "single")
.enemySpecies(SpeciesId.MIGHTYENA)
.enemyAbility(AbilityId.HYDRATION)
.ability(AbilityId.HYDRATION);
it("1vs1", async () => {
game.override.battleStyle("single");
game.override.enemySpecies(SpeciesId.MIGHTYENA);
game.override.enemyAbility(AbilityId.HYDRATION);
game.override.ability(AbilityId.HYDRATION);
await game.classicMode.startBattle([SpeciesId.BLASTOISE]);
expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND);
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name);
}, 20000);
await game.classicMode.startBattle(
[SpeciesId.BLASTOISE, SpeciesId.CHARIZARD, SpeciesId.DARKRAI, SpeciesId.GABITE].slice(0, qty),
);
it("2vs2", async () => {
game.override.battleStyle("double");
game.override.enemySpecies(SpeciesId.MIGHTYENA);
game.override.enemyAbility(AbilityId.HYDRATION);
game.override.ability(AbilityId.HYDRATION);
game.override.startingWave(3);
await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]);
expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND);
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name);
}, 20000);
it("4vs2", async () => {
game.override.battleStyle("double");
game.override.enemySpecies(SpeciesId.MIGHTYENA);
game.override.enemyAbility(AbilityId.HYDRATION);
game.override.ability(AbilityId.HYDRATION);
game.override.startingWave(3);
await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD, SpeciesId.DARKRAI, SpeciesId.GABITE]);
expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND);
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name);
}, 20000);
expect(game.scene.phaseManager.getCurrentPhase()).toBeInstanceOf(CommandPhase);
});
it("kill opponent pokemon", async () => {
const moveToUse = MoveId.SPLASH;

View File

@ -1,11 +1,14 @@
import { AbilityId } from "#enums/ability-id";
import { RandomMoveAttr } from "#app/data/moves/move";
import { MoveResult } from "#enums/move-result";
import { getPokemonNameWithAffix } from "#app/messages";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { AbilityId } from "#app/enums/ability-id";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager";
import i18next from "i18next";
import Phaser from "phaser";
//import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Chilly Reception", () => {
let phaserGame: Phaser.Game;
@ -25,95 +28,121 @@ describe("Moves - Chilly Reception", () => {
game = new GameManager(phaserGame);
game.override
.battleStyle("single")
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE])
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE, MoveId.SPLASH, MoveId.METRONOME])
.enemyMoveset(MoveId.SPLASH)
.enemyAbility(AbilityId.BALL_FETCH)
.ability(AbilityId.BALL_FETCH);
});
it("should still change the weather if user can't switch out", async () => {
it("should display message before use, switch the user out and change the weather to snow", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
it("should still change weather if user can't switch out", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING]);
game.move.select(MoveId.CHILLY_RECEPTION);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
});
it("should switch out even if it's snowing", async () => {
it("should still switch out even if weather cannot be changed", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
// first turn set up snow with snowscape, try chilly reception on second turn
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.SNOWSCAPE);
await game.phaseInterceptor.to("BerryPhase", false);
await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
// TODO: Uncomment lines once wimp out PR fixes force switches to not reset summon data immediately
// await game.phaseInterceptor.to("SwitchSummonPhase", false);
// expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
});
it("happy case - switch out and weather changes", async () => {
// Source: https://replay.pokemonshowdown.com/gen9ou-2367532550
it("should fail (while still displaying message) if neither weather change nor switch out succeeds", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING]);
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW);
const slowking = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SNOWSCAPE);
await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()).toBe(slowking);
expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
it("should succeed without message if called indirectly", async () => {
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.CHILLY_RECEPTION);
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.METRONOME);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH);
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.textInterceptor.logs).not.toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
// enemy uses another move and weather doesn't change
it("check case - enemy not selecting chilly reception doesn't change weather ", async () => {
game.override.battleStyle("single").enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]).moveset(MoveId.SPLASH);
// Bugcheck test for enemy AI bug
it("check case - enemy not selecting chilly reception doesn't change weather", async () => {
game.override.enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]);
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
game.move.select(MoveId.SPLASH);
await game.move.selectEnemyMove(MoveId.TACKLE);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(undefined);
});
it("enemy trainer - expected behavior ", async () => {
game.override
.battleStyle("single")
.startingWave(8)
.enemyMoveset(MoveId.CHILLY_RECEPTION)
.enemySpecies(SpeciesId.MAGIKARP)
.moveset([MoveId.SPLASH, MoveId.THUNDERBOLT]);
await game.classicMode.startBattle([SpeciesId.JOLTEON]);
const RIVAL_MAGIKARP1 = game.scene.getEnemyPokemon()?.id;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_MAGIKARP1);
await game.phaseInterceptor.to("TurnInitPhase", false);
game.move.select(MoveId.SPLASH);
// second chilly reception should still switch out
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
expect(game.scene.getEnemyPokemon()?.id === RIVAL_MAGIKARP1);
game.move.select(MoveId.THUNDERBOLT);
// enemy chilly recep move should fail: it's snowing and no option to switch out
// no crashing
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.arena.weather?.weatherType).toBeUndefined();
});
});

View File

@ -209,9 +209,9 @@ export class OverridesHelper extends GameManagerHelper {
}
/**
* Override the player pokemon's {@linkcode StatusEffect | status-effect}
* Override the player pokemon's initial {@linkcode StatusEffect | status-effect},
* @param statusEffect - The {@linkcode StatusEffect | status-effect} to set
* @returns
* @returns `this`
*/
public statusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
@ -401,9 +401,9 @@ export class OverridesHelper extends GameManagerHelper {
}
/**
* Override the enemy {@linkcode StatusEffect | status-effect} for enemy pokemon
* Override the enemy pokemon's initial {@linkcode StatusEffect | status-effect}.
* @param statusEffect - The {@linkcode StatusEffect | status-effect} to set
* @returns
* @returns `this`
*/
public enemyStatusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);