[Refactor/Bug] Pokemon.leaveField(), Fix Related Abilities (#5191)

* Added new AbAttr that triggers whenever a pokemon leaves the field

* Use leaveField everywhere

* Changing order for PreSwitchOutAbAttr

* Don't clearEffects when catching in a mystery encounter

* Attempts to make new overrides for testing

* New options in overrides

* Implemented tests for Desolate Land

* Fixing instruct test to not read turnData of fainted mon

* Removed post faint clear weather

* Apply suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Has_passive_ability override now turns off passives if set to "false", defaults to "null"

* Updating overrides type definitions

* Apply suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Suggestions from review

* Fixed strings in suggestions

* Simplified function to throw balls in tests

* Added tsdocs to overrideHelper.ts

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Wlowscha 2025-02-07 00:37:50 +01:00 committed by GitHub
parent 60990deaf2
commit 6c4dedb73e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 286 additions and 133 deletions

View File

@ -2643,55 +2643,6 @@ export class PreSwitchOutResetStatusAbAttr extends PreSwitchOutAbAttr {
}
}
/**
* Clears Desolate Land/Primordial Sea/Delta Stream upon the Pokemon switching out.
*/
export class PreSwitchOutClearWeatherAbAttr extends PreSwitchOutAbAttr {
/**
* @param pokemon The {@linkcode Pokemon} with the ability
* @param passive N/A
* @param args N/A
* @returns {boolean} Returns true if the weather clears, otherwise false.
*/
applyPreSwitchOut(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
const weatherType = globalScene.arena.weather?.weatherType;
let turnOffWeather = false;
// Clear weather only if user's ability matches the weather and no other pokemon has the ability.
switch (weatherType) {
case (WeatherType.HARSH_SUN):
if (pokemon.hasAbility(Abilities.DESOLATE_LAND)
&& globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.DESOLATE_LAND)).length === 0) {
turnOffWeather = true;
}
break;
case (WeatherType.HEAVY_RAIN):
if (pokemon.hasAbility(Abilities.PRIMORDIAL_SEA)
&& globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.PRIMORDIAL_SEA)).length === 0) {
turnOffWeather = true;
}
break;
case (WeatherType.STRONG_WINDS):
if (pokemon.hasAbility(Abilities.DELTA_STREAM)
&& globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.DELTA_STREAM)).length === 0) {
turnOffWeather = true;
}
break;
}
if (simulated) {
return turnOffWeather;
}
if (turnOffWeather) {
globalScene.arena.trySetWeather(WeatherType.NONE, false);
return true;
}
return false;
}
}
export class PreSwitchOutHealAbAttr extends PreSwitchOutAbAttr {
applyPreSwitchOut(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
@ -2744,6 +2695,61 @@ export class PreSwitchOutFormChangeAbAttr extends PreSwitchOutAbAttr {
}
export class PreLeaveFieldAbAttr extends AbAttr {
applyPreLeaveField(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
return false;
}
}
/**
* Clears Desolate Land/Primordial Sea/Delta Stream upon the Pokemon switching out.
*/
export class PreLeaveFieldClearWeatherAbAttr extends PreLeaveFieldAbAttr {
/**
* @param pokemon The {@linkcode Pokemon} with the ability
* @param passive N/A
* @param args N/A
* @returns Returns `true` if the weather clears, otherwise `false`.
*/
applyPreLeaveField(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
const weatherType = globalScene.arena.weather?.weatherType;
let turnOffWeather = false;
// Clear weather only if user's ability matches the weather and no other pokemon has the ability.
switch (weatherType) {
case (WeatherType.HARSH_SUN):
if (pokemon.hasAbility(Abilities.DESOLATE_LAND)
&& globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.DESOLATE_LAND)).length === 0) {
turnOffWeather = true;
}
break;
case (WeatherType.HEAVY_RAIN):
if (pokemon.hasAbility(Abilities.PRIMORDIAL_SEA)
&& globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.PRIMORDIAL_SEA)).length === 0) {
turnOffWeather = true;
}
break;
case (WeatherType.STRONG_WINDS):
if (pokemon.hasAbility(Abilities.DELTA_STREAM)
&& globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.DELTA_STREAM)).length === 0) {
turnOffWeather = true;
}
break;
}
if (simulated) {
return turnOffWeather;
}
if (turnOffWeather) {
globalScene.arena.trySetWeather(WeatherType.NONE, false);
return true;
}
return false;
}
}
export class PreStatStageChangeAbAttr extends AbAttr {
applyPreStatStageChange(pokemon: Pokemon | null, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise<boolean> {
return false;
@ -4171,59 +4177,6 @@ export class PostFaintUnsuppressedWeatherFormChangeAbAttr extends PostFaintAbAtt
}
}
/**
* Clears Desolate Land/Primordial Sea/Delta Stream upon the Pokemon fainting
*/
export class PostFaintClearWeatherAbAttr extends PostFaintAbAttr {
/**
* @param pokemon The {@linkcode Pokemon} with the ability
* @param passive N/A
* @param attacker N/A
* @param move N/A
* @param hitResult N/A
* @param args N/A
* @returns {boolean} Returns true if the weather clears, otherwise false.
*/
applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker?: Pokemon, move?: Move, hitResult?: HitResult, ...args: any[]): boolean {
const weatherType = globalScene.arena.weather?.weatherType;
let turnOffWeather = false;
// Clear weather only if user's ability matches the weather and no other pokemon has the ability.
switch (weatherType) {
case (WeatherType.HARSH_SUN):
if (pokemon.hasAbility(Abilities.DESOLATE_LAND)
&& globalScene.getField(true).filter(p => p.hasAbility(Abilities.DESOLATE_LAND)).length === 0) {
turnOffWeather = true;
}
break;
case (WeatherType.HEAVY_RAIN):
if (pokemon.hasAbility(Abilities.PRIMORDIAL_SEA)
&& globalScene.getField(true).filter(p => p.hasAbility(Abilities.PRIMORDIAL_SEA)).length === 0) {
turnOffWeather = true;
}
break;
case (WeatherType.STRONG_WINDS):
if (pokemon.hasAbility(Abilities.DELTA_STREAM)
&& globalScene.getField(true).filter(p => p.hasAbility(Abilities.DELTA_STREAM)).length === 0) {
turnOffWeather = true;
}
break;
}
if (simulated) {
return turnOffWeather;
}
if (turnOffWeather) {
globalScene.arena.trySetWeather(WeatherType.NONE, false);
return true;
}
return false;
}
}
export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
private damageRatio: number;
@ -5229,6 +5182,11 @@ export function applyPreSwitchOutAbAttrs(attrType: Constructor<PreSwitchOutAbAtt
return applyAbAttrsInternal<PreSwitchOutAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, args), args, true, simulated);
}
export function applyPreLeaveFieldAbAttrs(attrType: Constructor<PreLeaveFieldAbAttr>,
pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<PreLeaveFieldAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, args), args, true, simulated);
}
export function applyPreStatStageChangeAbAttrs(attrType: Constructor<PreStatStageChangeAbAttr>,
pokemon: Pokemon | null, stat: BattleStat, cancelled: Utils.BooleanHolder, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<PreStatStageChangeAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), args, false, simulated);
@ -5912,20 +5870,17 @@ export function initAbilities() {
new Ability(Abilities.PRIMORDIAL_SEA, 6)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.HEAVY_RAIN)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.HEAVY_RAIN)
.attr(PreSwitchOutClearWeatherAbAttr)
.attr(PostFaintClearWeatherAbAttr)
.attr(PreLeaveFieldClearWeatherAbAttr)
.bypassFaint(),
new Ability(Abilities.DESOLATE_LAND, 6)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.HARSH_SUN)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.HARSH_SUN)
.attr(PreSwitchOutClearWeatherAbAttr)
.attr(PostFaintClearWeatherAbAttr)
.attr(PreLeaveFieldClearWeatherAbAttr)
.bypassFaint(),
new Ability(Abilities.DELTA_STREAM, 6)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.STRONG_WINDS)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.STRONG_WINDS)
.attr(PreSwitchOutClearWeatherAbAttr)
.attr(PostFaintClearWeatherAbAttr)
.attr(PreLeaveFieldClearWeatherAbAttr)
.bypassFaint(),
new Ability(Abilities.STAMINA, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),

View File

@ -148,7 +148,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
// Adds a real Pokemon sprite to the field (required for the animation)
globalScene.getEnemyParty().forEach(enemyPokemon => {
globalScene.field.remove(enemyPokemon, true);
enemyPokemon.leaveField(true, true, true);
});
globalScene.currentBattle.enemyParty = [ oricorio ];
globalScene.field.add(oricorio);

View File

@ -229,7 +229,7 @@ function handleLoseMinigame() {
// End the battle
if (wobbuffet) {
wobbuffet.hideInfo();
globalScene.field.remove(wobbuffet);
wobbuffet.leaveField();
}
transitionMysteryEncounterIntroVisuals(true, true);
globalScene.currentBattle.enemyParty = [];
@ -278,7 +278,7 @@ function handleNextTurn() {
// End the battle
wobbuffet.hideInfo();
globalScene.field.remove(wobbuffet);
wobbuffet.leaveField();
globalScene.currentBattle.enemyParty = [];
globalScene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined;
leaveEncounterWithoutBattle(isHealPhase);

View File

@ -575,7 +575,7 @@ function onGameOver() {
ease: "Sine.easeIn",
scale: 0.5,
onComplete: () => {
globalScene.field.remove(pokemon, true);
pokemon.leaveField(true, true, true);
}
});
}

View File

@ -164,7 +164,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
}
globalScene.getEnemyParty().forEach(enemyPokemon => {
globalScene.field.remove(enemyPokemon, true);
enemyPokemon.leaveField(true, true, true);
});
battle.enemyParty = [];
battle.double = doubleBattle;
@ -810,7 +810,7 @@ export function transitionMysteryEncounterIntroVisuals(hide: boolean = true, des
globalScene.field.remove(introVisuals, true);
enemyPokemon.forEach(pokemon => {
globalScene.field.remove(pokemon, true);
pokemon.leaveField(true, true, true);
});
globalScene.currentBattle.mysteryEncounter!.introVisuals = undefined;

View File

@ -592,7 +592,7 @@ export async function catchPokemon(pokemon: EnemyPokemon, pokeball: Phaser.GameO
};
const removePokemon = () => {
if (pokemon) {
globalScene.field.remove(pokemon, true);
pokemon.leaveField(false, true, true);
}
};
const addToParty = (slotIndex?: number) => {
@ -695,7 +695,7 @@ export async function doPokemonFlee(pokemon: EnemyPokemon): Promise<void> {
scale: pokemon.getSpriteScale(),
onComplete: () => {
pokemon.setVisible(false);
globalScene.field.remove(pokemon, true);
pokemon.leaveField(true, true, true);
showEncounterText(i18next.t("battle:pokemonFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false)
.then(() => {
resolve();
@ -723,7 +723,7 @@ export function doPlayerFlee(pokemon: EnemyPokemon): Promise<void> {
scale: pokemon.getSpriteScale(),
onComplete: () => {
pokemon.setVisible(false);
globalScene.field.remove(pokemon, true);
pokemon.leaveField(true, true, true);
showEncounterText(i18next.t("battle:playerFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false)
.then(() => {
resolve();

View File

@ -31,7 +31,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo
import { WeatherType } from "#enums/weather-type";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
import type { Ability, AbAttr } from "#app/data/ability";
import { StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability";
import { StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs } from "#app/data/ability";
import type PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui";
@ -1422,8 +1422,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
*/
public hasPassive(): boolean {
// returns override if valid for current case
if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer())
|| (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) {
if (
(Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer())
|| (Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE === false && !this.isPlayer())
) {
return false;
}
if (
((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) && this.isPlayer())
|| ((Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE || Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE) && !this.isPlayer())
) {
return true;
}
@ -4142,9 +4150,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param hideInfo Indicates if this should also play the animation to hide the Pokemon's
* info container.
*/
leaveField(clearEffects: boolean = true, hideInfo: boolean = true) {
leaveField(clearEffects: boolean = true, hideInfo: boolean = true, destroy: boolean = false) {
this.resetSprite();
this.resetTurnData();
globalScene.getField(true).filter(p => p !== this).forEach(p => p.removeTagsBySourceId(this.id));
if (clearEffects) {
this.destroySubstitute();
this.resetSummonData(); // this also calls `resetBattleSummonData`
@ -4152,9 +4162,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (hideInfo) {
this.hideInfo();
}
globalScene.field.remove(this);
// Trigger abilities that activate upon leaving the field
applyPreLeaveFieldAbAttrs(PreLeaveFieldAbAttr, this);
this.setSwitchOutStatus(true);
globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true);
globalScene.field.remove(this, destroy);
}
destroy(): void {

View File

@ -129,6 +129,7 @@ class DefaultOverrides {
readonly STARTER_FUSION_SPECIES_OVERRIDE: Species | number = 0;
readonly ABILITY_OVERRIDE: Abilities = Abilities.NONE;
readonly PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE;
readonly HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null;
readonly STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
readonly GENDER_OVERRIDE: Gender | null = null;
readonly MOVESET_OVERRIDE: Moves | Array<Moves> = [];
@ -150,6 +151,7 @@ class DefaultOverrides {
readonly OPP_LEVEL_OVERRIDE: number = 0;
readonly OPP_ABILITY_OVERRIDE: Abilities = Abilities.NONE;
readonly OPP_PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE;
readonly OPP_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null;
readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
readonly OPP_GENDER_OVERRIDE: Gender | null = null;
readonly OPP_MOVESET_OVERRIDE: Moves | Array<Moves> = [];

View File

@ -241,11 +241,10 @@ export class AttemptCapturePhase extends PokemonPhase {
};
const removePokemon = () => {
globalScene.addFaintedEnemyScore(pokemon);
globalScene.getPlayerField().filter(p => p.isActive(true)).forEach(playerPokemon => playerPokemon.removeTagsBySourceId(pokemon.id));
pokemon.hp = 0;
pokemon.trySetStatus(StatusEffect.FAINT);
globalScene.clearEnemyHeldItemModifiers();
globalScene.field.remove(pokemon, true);
pokemon.leaveField(true, true, true);
};
const addToParty = (slotIndex?: number) => {
const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex);

View File

@ -181,9 +181,7 @@ export class FaintPhase extends PokemonPhase {
y: pokemon.y + 150,
ease: "Sine.easeIn",
onComplete: () => {
pokemon.resetSprite();
pokemon.lapseTags(BattlerTagLapseType.FAINT);
globalScene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id));
pokemon.y -= 150;
pokemon.trySetStatus(StatusEffect.FAINT);
@ -193,7 +191,7 @@ export class FaintPhase extends PokemonPhase {
globalScene.addFaintedEnemyScore(pokemon as EnemyPokemon);
globalScene.currentBattle.addPostBattleLoot(pokemon as EnemyPokemon);
}
globalScene.field.remove(pokemon);
pokemon.leaveField();
this.end();
}
});

View File

@ -64,6 +64,7 @@ export class SwitchSummonPhase extends SummonPhase {
const pokemon = this.getPokemon();
(this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) {
const substitute = pokemon.getTag(SubstituteTag);
if (substitute) {
@ -93,8 +94,8 @@ export class SwitchSummonPhase extends SummonPhase {
ease: "Sine.easeIn",
scale: 0.5,
onComplete: () => {
pokemon.leaveField(this.switchType === SwitchType.SWITCH, false);
globalScene.time.delayedCall(750, () => this.switchAndSummon());
pokemon.leaveField(this.switchType === SwitchType.SWITCH, false);
}
});
}

View File

@ -0,0 +1,139 @@
import { PokeballType } from "#app/enums/pokeball";
import { WeatherType } from "#app/enums/weather-type";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
describe("Abilities - Desolate Land", () => {
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
.moveset(Moves.SPLASH)
.hasPassiveAbility(true)
.enemySpecies(Species.RALTS)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
/**
* This checks that the weather has changed after the Enemy Pokemon with {@linkcode Abilities.DESOLATE_LAND}
* is forcefully moved out of the field from moves such as Roar {@linkcode Moves.ROAR}
*/
it("should lift only when all pokemon with this ability leave the field", async () => {
game.override
.battleType("double")
.enemyMoveset([ Moves.SPLASH, Moves.ROAR ]);
await game.classicMode.startBattle([ Species.MAGCARGO, Species.MAGCARGO, Species.MAGIKARP, Species.MAGIKARP ]);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN);
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH, 0, 2);
game.move.select(Moves.SPLASH, 1, 2);
await game.forceEnemyMove(Moves.ROAR, 0);
await game.forceEnemyMove(Moves.SPLASH, 1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN);
await game.toNextTurn();
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min + 1;
});
game.move.select(Moves.SPLASH, 0, 2);
game.move.select(Moves.SPLASH, 1, 2);
await game.forceEnemyMove(Moves.ROAR, 1);
await game.forceEnemyMove(Moves.SPLASH, 0);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN);
});
it("should lift when enemy faints", async () => {
game.override
.battleType("single")
.moveset([ Moves.SHEER_COLD ])
.ability(Abilities.NO_GUARD)
.startingLevel(100)
.enemyLevel(1)
.enemyMoveset([ Moves.SPLASH ])
.enemySpecies(Species.MAGCARGO)
.enemyHasPassiveAbility(true);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN);
game.move.select(Moves.SHEER_COLD);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN);
});
it("should lift when pokemon returns upon switching from double to single battle", async () => {
game.override
.battleType("even-doubles")
.enemyMoveset([ Moves.SPLASH, Moves.MEMENTO ])
.startingWave(12);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGCARGO ]);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN);
game.move.select(Moves.SPLASH, 0, 2);
game.move.select(Moves.SPLASH, 1, 2);
await game.forceEnemyMove(Moves.MEMENTO, 0);
await game.forceEnemyMove(Moves.MEMENTO, 1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN);
await game.toNextWave();
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN);
});
it("should lift when enemy is captured", async () => {
game.override
.battleType("single")
.enemyMoveset([ Moves.SPLASH ])
.enemySpecies(Species.MAGCARGO)
.enemyHasPassiveAbility(true);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN);
game.scene.pokeballCounts[PokeballType.MASTER_BALL] = 1;
game.doThrowPokeball(PokeballType.MASTER_BALL);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN);
});
});

View File

@ -221,7 +221,8 @@ describe("Moves - Instruct", () => {
it("should allow for dancer copying of instructed dance move", async () => {
game.override
.battleType("double")
.enemyMoveset([ Moves.INSTRUCT, Moves.SPLASH ]);
.enemyMoveset([ Moves.INSTRUCT, Moves.SPLASH ])
.enemyLevel(1000);
await game.classicMode.startBattle([ Species.ORICORIO, Species.VOLCARONA ]);
const [ oricorio, volcarona ] = game.scene.getPlayerField();
@ -236,11 +237,9 @@ describe("Moves - Instruct", () => {
await game.phaseInterceptor.to("BerryPhase");
// fiery dance triggered dancer successfully for a total of 4 hits
// Volcarona fiery dance has a _small_ chance to 3HKO a shuckle in worst case, so we add the hit count of both
// foes to account for spillover
// Enemy level is set to a high value so that it does not faint even after all 4 hits
instructSuccess(volcarona, Moves.FIERY_DANCE);
expect(game.scene.getEnemyField()[0].turnData.attacksReceived.length +
game.scene.getEnemyField()[1].turnData.attacksReceived.length).toBe(4);
expect(game.scene.getEnemyField()[0].turnData.attacksReceived.length).toBe(4);
});
it("should not repeat move when switching out", async () => {

View File

@ -24,6 +24,7 @@ import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
import ErrorInterceptor from "#app/test/utils/errorInterceptor";
import type InputsHandler from "#app/test/utils/inputsHandler";
import type BallUiHandler from "#app/ui/ball-ui-handler";
import type BattleMessageUiHandler from "#app/ui/battle-message-ui-handler";
import type CommandUiHandler from "#app/ui/command-ui-handler";
import type ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
@ -458,6 +459,24 @@ export default class GameManager {
});
}
/**
* Select the BALL option from the command menu, then press Action; in the BALL
* menu, select a pokéball type and press Action again to throw it.
* @param ballIndex the index of the pokeball to throw
*/
public doThrowPokeball(ballIndex: number) {
this.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
(this.scene.ui.getHandler() as CommandUiHandler).setCursor(1);
(this.scene.ui.getHandler() as CommandUiHandler).processInput(Button.ACTION);
});
this.onNextPrompt("CommandPhase", Mode.BALL, () => {
const ballHandler = this.scene.ui.getHandler() as BallUiHandler;
ballHandler.setCursor(ballIndex);
ballHandler.processInput(Button.ACTION); // select ball and throw
});
}
/**
* Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value.
* Used to manually modify Pokemon turn order.

View File

@ -181,6 +181,20 @@ export class OverridesHelper extends GameManagerHelper {
return this;
}
/**
* Forces the status of the player (pokemon) **passive** {@linkcode Abilities | ability}
* @param hasPassiveAbility forces the passive to be active if `true`, inactive if `false`
* @returns `this`
*/
public hasPassiveAbility(hasPassiveAbility: boolean | null): this {
vi.spyOn(Overrides, "HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility);
if (hasPassiveAbility === null) {
this.log("Player Pokemon PASSIVE ability no longer force enabled or disabled!");
} else {
this.log(`Player Pokemon PASSIVE ability is force ${hasPassiveAbility ? "enabled" : "disabled"}!`);
}
return this;
}
/**
* Override the player (pokemon) {@linkcode Moves | moves}set
* @param moveset the {@linkcode Moves | moves}set to set
@ -325,6 +339,21 @@ export class OverridesHelper extends GameManagerHelper {
return this;
}
/**
* Forces the status of the enemy (pokemon) **passive** {@linkcode Abilities | ability}
* @param hasPassiveAbility forces the passive to be active if `true`, inactive if `false`
* @returns `this`
*/
public enemyHasPassiveAbility(hasPassiveAbility: boolean | null): this {
vi.spyOn(Overrides, "OPP_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility);
if (hasPassiveAbility === null) {
this.log("Enemy Pokemon PASSIVE ability no longer force enabled or disabled!");
} else {
this.log(`Enemy Pokemon PASSIVE ability is force ${hasPassiveAbility ? "enabled" : "disabled"}!`);
}
return this;
}
/**
* Override the enemy (pokemon) {@linkcode Moves | moves}set
* @param moveset the {@linkcode Moves | moves}set to set