diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 16498bcd9c4..d82f7706806 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -897,6 +897,12 @@ export default class BattleScene extends SceneBase { return pokemon; } + /** + * Removes a PlayerPokemon from the party, and clears modifiers for that Pokemon's id + * Useful for MEs/Challenges that remove Pokemon from the player party temporarily or permanently + * @param pokemon + * @param destroy - Default true. If true, will destroy the Pokemon object after removing + */ removePokemonFromPlayerParty(pokemon: PlayerPokemon, destroy: boolean = true) { if (!pokemon) { return; @@ -1113,7 +1119,7 @@ export default class BattleScene extends SceneBase { } } - newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean, mysteryEncounter?: MysteryEncounter): Battle | null { + newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean, mysteryEncounterType?: MysteryEncounterType): Battle | null { const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave; const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1); let newDouble: boolean | undefined; @@ -1236,7 +1242,7 @@ export default class BattleScene extends SceneBase { // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) this.currentBattle.double = false; this.executeWithSeedOffset(() => { - this.currentBattle.mysteryEncounter = this.getMysteryEncounter(mysteryEncounter); + this.currentBattle.mysteryEncounter = this.getMysteryEncounter(mysteryEncounterType); }, this.currentBattle.waveIndex << 4); } @@ -3016,16 +3022,16 @@ export default class BattleScene extends SceneBase { /** * Loads or generates a mystery encounter - * @param override - used to load session encounter when restarting game, etc. + * @param encounterType - used to load session encounter when restarting game, etc. * @returns */ - getMysteryEncounter(override: MysteryEncounter | undefined): MysteryEncounter { + getMysteryEncounter(encounterType?: MysteryEncounterType): MysteryEncounter { // Loading override or session encounter let encounter: MysteryEncounter | null; if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE!)) { encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE!]; } else { - encounter = override?.encounterType && override.encounterType >= 0 ? allMysteryEncounters[override.encounterType] : null; + encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType!] : null; } // Check for queued encounters first diff --git a/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts index 4e933304c5e..7afcdb7a79e 100644 --- a/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts +++ b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts @@ -2,15 +2,15 @@ import { Abilities } from "#enums/abilities"; import { Type } from "#app/data/type"; export class MysteryEncounterPokemonData { - public spriteScale: number | undefined; - public ability: Abilities | undefined; - public passive: Abilities | undefined; + public spriteScale: number; + public ability: Abilities | -1; + public passive: Abilities | -1; public types: Type[]; constructor(spriteScale?: number, ability?: Abilities, passive?: Abilities, types?: Type[]) { - this.spriteScale = spriteScale; - this.ability = ability; - this.passive = passive; + this.spriteScale = spriteScale ?? -1; + this.ability = ability ?? -1; + this.passive = passive ?? -1; this.types = types ?? []; } } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0d9b39654f0..f47e56298f4 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5,7 +5,7 @@ import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } from "../data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; -import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; +import { Constructor, randSeedInt } from "#app/utils"; import * as Utils from "../utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type"; import { getLevelTotalExp } from "../data/exp"; @@ -223,6 +223,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.variant = this.shiny ? this.generateVariant() : 0; } + this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + if (nature !== undefined) { this.setNature(nature); } else { @@ -250,7 +252,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0); this.fusionLuck = this.luck; - this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); } this.generateName(); @@ -578,8 +579,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const formKey = this.getFormKey(); if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1) { return 1.5; - } else if (!isNullOrUndefined(this.mysteryEncounterPokemonData.spriteScale) && this.mysteryEncounterPokemonData.spriteScale !== 0) { - return this.mysteryEncounterPokemonData.spriteScale!; + } else if (this.mysteryEncounterPokemonData.spriteScale > 0) { + return this.mysteryEncounterPokemonData.spriteScale; } return 1; } @@ -1150,7 +1151,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; } - if (this.mysteryEncounterPokemonData?.ability) { + if (this.mysteryEncounterPokemonData.ability !== -1) { return allAbilities[this.mysteryEncounterPokemonData.ability]; } if (this.isFusion()) { @@ -1177,7 +1178,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; } - if (this.mysteryEncounterPokemonData?.passive) { + if (this.mysteryEncounterPokemonData.passive !== -1) { return allAbilities[this.mysteryEncounterPokemonData.passive]; } diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 2c8777ecfe1..8ab191324c6 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -238,7 +238,7 @@ export class GameOverPhase extends BattlePhase { gameVersion: this.scene.game.config.gameVersion, timestamp: new Date().getTime(), challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)), - mysteryEncounter: this.scene.currentBattle.mysteryEncounter, + mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType, mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData } as SessionSaveData; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index b21c8b0e2a3..03679ca21e4 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -47,7 +47,7 @@ import { ReloadSessionPhase } from "#app/phases/reload-session-phase"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; import { applySessionDataPatches, applySettingsDataPatches, applySystemDataPatches } from "./version-converter"; import { MysteryEncounterSaveData } from "../data/mystery-encounters/mystery-encounter-save-data"; -import MysteryEncounter from "../data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, @@ -132,7 +132,7 @@ export interface SessionSaveData { gameVersion: string; timestamp: integer; challenges: ChallengeData[]; - mysteryEncounter: MysteryEncounter; + mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, mysteryEncounterSaveData: MysteryEncounterSaveData; } @@ -952,7 +952,7 @@ export class GameData { gameVersion: scene.game.config.gameVersion, timestamp: new Date().getTime(), challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)), - mysteryEncounter: scene.currentBattle.mysteryEncounter, + mysteryEncounterType: scene.currentBattle.mysteryEncounter?.encounterType, mysteryEncounterSaveData: scene.mysteryEncounterSaveData } as SessionSaveData; } @@ -1050,8 +1050,8 @@ export class GameData { const battleType = sessionData.battleType || 0; const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; - const mysteryEncounterConfig = sessionData?.mysteryEncounter; - const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1, mysteryEncounterConfig)!; // TODO: is this bang correct? + const mysteryEncounterType = sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined; + const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1, mysteryEncounterType)!; // TODO: is this bang correct? battle.enemyLevels = sessionData.enemyParty.map(p => p.level); scene.arena.init(); @@ -1263,8 +1263,8 @@ export class GameData { return ret; } - if (k === "mysteryEncounter") { - return new MysteryEncounter(v); + if (k === "mysteryEncounterType") { + return v as MysteryEncounterType; } if (k === "mysteryEncounterSaveData") { diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index 3bf17b61b2a..7cca7abba27 100644 --- a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -185,7 +185,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { }); }); - it("Should return 3 (2/5ths floored) berries if 8 were stolen", async () => { + it("Should return 3 (2/5ths floored) berries if 8 were stolen", {retry: 5}, async () => { game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 3, type: BerryType.APICOT}]); await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); @@ -201,7 +201,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { expect(berryCountAfter).toBe(3); }); - it("Should return 2 (2/5ths floored) berries if 7 were stolen", async () => { + it("Should return 2 (2/5ths floored) berries if 7 were stolen", {retry: 5}, async () => { game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 2, type: BerryType.APICOT}]); await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index a106ef7615f..1c68852a63d 100644 --- a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -16,6 +16,7 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Moves } from "#enums/moves"; import { ShinyRateBoosterModifier } from "#app/modifier/modifier"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; const namespace = "mysteryEncounter:offerYouCantRefuse"; /** Gyarados for Indimidate */ @@ -203,6 +204,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { const expBefore = gyarados.exp; await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); expect(gyarados.exp).toBe(expBefore + Math.floor(getPokemonSpecies(Species.LIEPARD).baseExp * defaultWave / 5 + 1)); }); @@ -215,6 +217,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { const expBefore = abra.exp; await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); expect(abra.exp).toBe(expBefore + Math.floor(getPokemonSpecies(Species.LIEPARD).baseExp * defaultWave / 5 + 1)); }); diff --git a/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts index 2f13105047d..3d39b9f5bcb 100644 --- a/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -103,8 +103,7 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give no reward on incorrect option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 2 }); - expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectModifierPhase); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; @@ -114,8 +113,7 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give proper rewards on correct Physical move option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); - expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectModifierPhase); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; @@ -151,8 +149,7 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give no reward on incorrect option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); - expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectModifierPhase); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; @@ -162,8 +159,7 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give proper rewards on correct Special move option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 2 }); - expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectModifierPhase); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; @@ -199,8 +195,7 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give no reward on incorrect option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectModifierPhase); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; @@ -210,8 +205,7 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give proper rewards on correct Special move option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 3 }); - expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectModifierPhase); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index f8ad13eb046..492299fe4da 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -14,6 +14,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import BattleScene from "#app/battle-scene"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { PartyExpPhase } from "#app/phases/party-exp-phase"; const namespace = "mysteryEncounter:lostAtSea"; /** Blastoise for surf. Pidgeot for fly. Abra for none. */ @@ -133,6 +134,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const expBefore = blastoise!.exp; await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(PartyExpPhase); expect(blastoise?.exp).toBe(expBefore + Math.floor(laprasSpecies.baseExp * defaultWave / 5 + 1)); }); @@ -197,6 +199,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const expBefore = pidgeot!.exp; await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(PartyExpPhase); expect(pidgeot!.exp).toBe(expBefore + Math.floor(laprasBaseExp * defaultWave / 5 + 1)); }); diff --git a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index bf09b94a3ff..ada4f32981e 100644 --- a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -19,6 +19,8 @@ import { MovePhase } from "#app/phases/move-phase"; import { speciesEggMoves } from "#app/data/egg-moves"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { BerryType } from "#enums/berry-type"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; const namespace = "mysteryEncounter:uncommonBreed"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; @@ -119,6 +121,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { it("should start a fight against the boss", async () => { const phaseSpy = vi.spyOn(scene, "pushPhase"); + const unshiftPhaseSpy = vi.spyOn(scene, "unshiftPhase"); await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; @@ -130,7 +133,9 @@ describe("Uncommon Breed - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(enemyField.length).toBe(1); expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); - expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); + + const statStagePhases = unshiftPhaseSpy.mock.calls.filter(p => p[0] instanceof StatStageChangePhase)[0][0] as any; + expect(statStagePhases.stats).toEqual([Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]); // Should have used its egg move pre-battle const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);