mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-20 16:42:45 +02:00
Compare commits
11 Commits
16c6df3e39
...
3f31f0e13e
Author | SHA1 | Date | |
---|---|---|---|
|
3f31f0e13e | ||
|
1ff2701964 | ||
|
1e306e25b5 | ||
|
b393cf8914 | ||
|
43aa772603 | ||
|
f231755db2 | ||
|
6d777bb7c8 | ||
|
fcd270ed9b | ||
|
86a3f6d711 | ||
|
12a626d2a5 | ||
|
be3c42a531 |
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -751,7 +751,7 @@ export async function catchPokemon(
|
||||
UiMode.POKEDEX_PAGE,
|
||||
pokemon.species,
|
||||
pokemon.formIndex,
|
||||
attributes,
|
||||
[attributes],
|
||||
null,
|
||||
() => {
|
||||
globalScene.ui.setMode(UiMode.MESSAGE).then(() => {
|
||||
|
@ -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 {
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -245,6 +245,7 @@ export async function initI18n(): Promise<void> {
|
||||
"pokeball",
|
||||
"pokedexUiHandler",
|
||||
"pokemon",
|
||||
"pokemonCategory",
|
||||
"pokemonEvolutions",
|
||||
"pokemonForm",
|
||||
"pokemonInfo",
|
||||
|
@ -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(() => {
|
||||
|
62
test/abilities/innards-out.test.ts
Normal file
62
test/abilities/innards-out.test.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user