diff --git a/src/account.ts b/src/account.ts index c63bf879e9f..e5bde56bfe3 100644 --- a/src/account.ts +++ b/src/account.ts @@ -9,6 +9,10 @@ export interface UserInfo { export let loggedInUser: UserInfo = null; export const clientSessionId = Utils.randomString(32); +export function initLoggedInUser(): void { + loggedInUser = { username: "Guest", lastSessionSlot: -1 }; +} + export function updateUserInfo(): Promise<[boolean, integer]> { return new Promise<[boolean, integer]>(resolve => { if (bypassLogin) { diff --git a/src/battle-scene.ts b/src/battle-scene.ts index f3adbda224e..73332e03ec6 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1922,6 +1922,8 @@ export default class BattleScene extends SceneBase { } if (!this.phaseQueue.length) { this.populatePhaseQueue(); + // clear the conditionalQueue if there are no phases left in the phaseQueue + this.conditionalQueue = []; } this.currentPhase = this.phaseQueue.shift(); diff --git a/src/phases.ts b/src/phases.ts index 8aa2bebe569..5c9c3ac9360 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1033,13 +1033,19 @@ export class EncounterPhase extends BattlePhase { if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => { - // is the player party initialized ? - const a = !!this.scene.getParty()?.length; + // if there is not a player party, we can't continue + if (!this.scene.getParty()?.length) { + return false; + } // how many player pokemon are on the field ? - const amountOnTheField = this.scene.getParty().filter(p => p.isOnField()).length; + const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length; + // if it's a 2vs1, there will never be a 2nd pokemon on our field even + const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2); // if it's a double, there should be 2, otherwise 1 - const b = this.scene.currentBattle.double ? amountOnTheField === 2 : amountOnTheField === 1; - return a && b; + if (this.scene.currentBattle.double) { + return pokemonsOnFieldCount === requiredPokemonsOnField; + } + return pokemonsOnFieldCount === 1; })); const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { diff --git a/src/test/abilities/intimidate.test.ts b/src/test/abilities/intimidate.test.ts index 0f7af6f6cd7..2409c64e5be 100644 --- a/src/test/abilities/intimidate.test.ts +++ b/src/test/abilities/intimidate.test.ts @@ -5,15 +5,17 @@ import * as overrides from "#app/overrides"; import {Abilities} from "#app/data/enums/abilities"; import {Species} from "#app/data/enums/species"; import { - CommandPhase, DamagePhase, - EnemyCommandPhase, + CommandPhase, DamagePhase, EncounterPhase, + EnemyCommandPhase, SelectStarterPhase, TurnInitPhase, } from "#app/phases"; import {Mode} from "#app/ui/ui"; import {BattleStat} from "#app/data/battle-stat"; import {Moves} from "#app/data/enums/moves"; -import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import {generateStarter, getMovePosition} from "#app/test/utils/gameManagerUtils"; import {Command} from "#app/ui/command-ui-handler"; +import {Status, StatusEffect} from "#app/data/status-effect"; +import {GameModes, getGameMode} from "#app/game-mode"; describe("Abilities - Intimidate", () => { @@ -337,5 +339,53 @@ describe("Abilities - Intimidate", () => { expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - }, 200000); + }, 20000); + + it("double - wild vs only 1 on player side", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + await game.runToSummon([ + Species.MIGHTYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, false); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; + expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1); + + const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + }, 20000); + + it("double - wild vs only 1 alive on player side", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + await game.runToTitle(); + + game.onNextPrompt("TitlePhase", Mode.TITLE, () => { + game.scene.gameMode = getGameMode(GameModes.CLASSIC); + const starters = generateStarter(game.scene, [ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + const selectStarterPhase = new SelectStarterPhase(game.scene); + game.scene.pushPhase(new EncounterPhase(game.scene, false)); + selectStarterPhase.initBattle(starters); + game.scene.getParty()[1].hp = 0; + game.scene.getParty()[1].status = new Status(StatusEffect.FAINT); + }); + + await game.phaseInterceptor.run(EncounterPhase); + + await game.phaseInterceptor.to(CommandPhase, false); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; + expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1); + + const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + }, 20000); }); diff --git a/src/test/phases/phases.test.ts b/src/test/phases/phases.test.ts index 009526ebe41..3d6da362004 100644 --- a/src/test/phases/phases.test.ts +++ b/src/test/phases/phases.test.ts @@ -28,7 +28,7 @@ describe("Phases", () => { describe("LoginPhase", () => { it("should start the login phase", async () => { const loginPhase = new LoginPhase(scene); - scene.pushPhase(loginPhase); + scene.unshiftPhase(loginPhase); await game.phaseInterceptor.run(LoginPhase); expect(scene.ui.getMode()).to.equal(Mode.MESSAGE); }); @@ -37,7 +37,7 @@ describe("Phases", () => { describe("TitlePhase", () => { it("should start the title phase", async () => { const titlePhase = new TitlePhase(scene); - scene.pushPhase(titlePhase); + scene.unshiftPhase(titlePhase); await game.phaseInterceptor.run(TitlePhase); expect(scene.ui.getMode()).to.equal(Mode.TITLE); }); @@ -46,7 +46,7 @@ describe("Phases", () => { describe("UnavailablePhase", () => { it("should start the unavailable phase", async () => { const unavailablePhase = new UnavailablePhase(scene); - scene.pushPhase(unavailablePhase); + scene.unshiftPhase(unavailablePhase); await game.phaseInterceptor.run(UnavailablePhase); expect(scene.ui.getMode()).to.equal(Mode.UNAVAILABLE); }, 20000); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 4f6056b1f14..418f8236033 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -6,7 +6,6 @@ import { EncounterPhase, FaintPhase, LoginPhase, NewBattlePhase, - SelectGenderPhase, SelectStarterPhase, TitlePhase, TurnInitPhase, } from "#app/phases"; @@ -100,14 +99,8 @@ export default class GameManager { * @returns A promise that resolves when the title phase is reached. */ async runToTitle(): Promise { - await this.phaseInterceptor.run(LoginPhase); - - this.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => { - this.scene.gameData.gender = PlayerGender.MALE; - this.endPhase(); - }, () => this.isCurrentPhase(TitlePhase)); - - await this.phaseInterceptor.run(SelectGenderPhase, () => this.isCurrentPhase(TitlePhase)); + await this.phaseInterceptor.whenAboutToRun(LoginPhase); + this.phaseInterceptor.pop(); await this.phaseInterceptor.run(TitlePhase); this.scene.gameSpeed = 5; @@ -116,6 +109,9 @@ export default class GameManager { this.scene.expGainsSpeed = 3; this.scene.expParty = ExpNotification.SKIP; this.scene.hpBarSpeed = 3; + this.scene.enableTutorials = false; + this.scene.gameData.gender = PlayerGender.MALE; + } /** diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 9e7745c8a37..e8cec7c9989 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -212,19 +212,20 @@ export default class PhaseInterceptor { return new Promise(async (resolve, reject) => { ErrorInterceptor.getInstance().add(this); const interval = setInterval(async () => { - const currentPhase = this.onHold.shift(); - if (currentPhase) { - if (currentPhase.name !== targetName) { - this.onHold.unshift(currentPhase); - } else { - clearInterval(interval); - resolve(); - } + const currentPhase = this.onHold[0]; + if (currentPhase?.name === targetName) { + clearInterval(interval); + resolve(); } }); }); } + pop() { + this.onHold.pop(); + this.scene.shiftPhase(); + } + /** * Method to initialize phases and their corresponding methods. */ diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index 8dac707255a..29af5b034b2 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -10,6 +10,7 @@ import {initMoves} from "#app/data/move"; import {initAbilities} from "#app/data/ability"; import {initAchievements} from "#app/system/achv.js"; import { initVouchers } from "#app/system/voucher.js"; +import {initLoggedInUser} from "#app/account"; initVouchers(); initAchievements(); @@ -21,5 +22,6 @@ initPokemonForms(); initSpecies(); initMoves(); initAbilities(); +initLoggedInUser(); global.testFailed = false;