diff --git a/package.json b/package.json index f100b3865d2..24aa6243b75 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "imports": { "#app": "./src/main.js", - "#app/*": "./src/*" + "#app/*": "./src/*", + "#test/*": "./src/test/*" } } diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 9a6cc40f153..f3adbda224e 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1098,18 +1098,18 @@ export default class BattleScene extends SceneBase { if (pokemon.hasAbility(Abilities.ICE_FACE)) { pokemon.formIndex = 0; } + + pokemon.resetBattleData(); + applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } + this.unshiftPhase(new ShowTrainerPhase(this)); } + for (const pokemon of this.getParty()) { - if (pokemon) { - if (resetArenaState) { - pokemon.resetBattleData(); - applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon, true); - } - this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger); - } + this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger); } + if (!this.gameMode.hasRandomBiomes && !isNewBiome) { this.pushPhase(new NextEncounterPhase(this)); } else { diff --git a/src/data/ability.ts b/src/data/ability.ts index c974f2c31d0..08f6712d4c7 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3525,7 +3525,7 @@ export class IceFaceMoveImmunityAbAttr extends MoveImmunityAbAttr { function applyAbAttrsInternal(attrType: { new(...args: any[]): TAttr }, pokemon: Pokemon, applyFunc: AbAttrApplyFunc, args: any[], isAsync: boolean = false, showAbilityInstant: boolean = false, quiet: boolean = false, passive: boolean = false): Promise { return new Promise(resolve => { - if (!pokemon.canApplyAbility(passive, args[0])) { + if (!pokemon.canApplyAbility(passive)) { if (!passive) { return applyAbAttrsInternal(attrType, pokemon, applyFunc, args, isAsync, showAbilityInstant, quiet, true).then(() => resolve()); } else { @@ -4238,7 +4238,8 @@ export function initAbilities() { .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr) - .attr(NoFusionAbilityAbAttr), + .attr(NoFusionAbilityAbAttr) + .bypassFaint(), new Ability(Abilities.VICTORY_STAR, 5) .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.1) .partial(), @@ -4347,6 +4348,7 @@ export function initAbilities() { .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) + .bypassFaint() .partial(), new Ability(Abilities.STAKEOUT, 7) .attr(MovePowerBoostAbAttr, (user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()].command === Command.POKEMON, 2), @@ -4379,7 +4381,8 @@ export function initAbilities() { .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr) - .attr(NoFusionAbilityAbAttr), + .attr(NoFusionAbilityAbAttr) + .bypassFaint(), new Ability(Abilities.DISGUISE, 7) .attr(PreDefendMovePowerToOneAbAttr, (target, user, move) => target.formIndex === 0 && target.getAttackTypeEffectiveness(move.type, user) > 0) .attr(PostSummonFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) @@ -4392,6 +4395,7 @@ export function initAbilities() { .attr(UnsuppressableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) + .bypassFaint() .ignorable() .partial(), new Ability(Abilities.BATTLE_BOND, 7) @@ -4400,7 +4404,8 @@ export function initAbilities() { .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr) - .attr(NoFusionAbilityAbAttr), + .attr(NoFusionAbilityAbAttr) + .bypassFaint(), new Ability(Abilities.POWER_CONSTRUCT, 7) // TODO: 10% Power Construct Zygarde isn't accounted for yet. If changed, update Zygarde's getSpeciesFormIndex entry accordingly .attr(PostBattleInitFormChangeAbAttr, () => 2) .attr(PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) @@ -4409,6 +4414,7 @@ export function initAbilities() { .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) + .bypassFaint() .partial(), new Ability(Abilities.CORROSION, 7) // TODO: Test Corrosion against Magic Bounce once it is implemented .attr(IgnoreTypeStatusEffectImmunityAbAttr, [StatusEffect.POISON, StatusEffect.TOXIC], [Type.STEEL, Type.POISON]) @@ -4639,7 +4645,8 @@ export function initAbilities() { .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .attr(PostBattleInitFormChangeAbAttr, () => 0) - .attr(PreSwitchOutFormChangeAbAttr, () => 1), + .attr(PreSwitchOutFormChangeAbAttr, () => 1) + .bypassFaint(), new Ability(Abilities.COMMANDER, 9) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 36659acf28c..58556e46a4e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1004,7 +1004,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} passive If true, check if passive can be applied instead of non-passive * @returns {Ability} The passive ability of the pokemon */ - canApplyAbility(passive: boolean = false, forceBypass: boolean = false): boolean { + canApplyAbility(passive: boolean = false): boolean { if (passive && !this.hasPassive()) { return false; } @@ -1032,7 +1032,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } } - return (this.hp || ability.isBypassFaint || forceBypass) && !ability.conditions.find(condition => !condition(this)); + return (this.hp || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); } /** diff --git a/src/overrides.ts b/src/overrides.ts index 676baaf0452..df95104387b 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -15,6 +15,7 @@ import {TimeOfDay} from "#app/data/enums/time-of-day"; import { Gender } from "./data/gender"; import { StatusEffect } from "./data/status-effect"; import { modifierTypes } from "./modifier/modifier-type"; +import { allSpecies } from "./data/pokemon-species"; // eslint-disable-line @typescript-eslint/no-unused-vars /** * Overrides for testing different in game situations @@ -53,8 +54,18 @@ export const POKEBALL_OVERRIDE: { active: boolean, pokeballs: PokeballCounts } = * PLAYER OVERRIDES */ -// forms can be found in pokemon-species.ts -export const STARTER_FORM_OVERRIDE: integer = 0; +/** + * Set the form index of any starter in the party whose `speciesId` is inside this override + * @see {@link allSpecies} in `src/data/pokemon-species.ts` for form indexes + * @example + * ``` + * const STARTER_FORM_OVERRIDES = { + * [Species.DARMANITAN]: 1 + * } + * ``` + */ +export const STARTER_FORM_OVERRIDES: Partial> = {}; + // default 5 or 20 for Daily export const STARTING_LEVEL_OVERRIDE: integer = 0; /** diff --git a/src/phases.ts b/src/phases.ts index 9e6653a8006..8aa2bebe569 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -555,6 +555,10 @@ export class SelectStarterPhase extends Phase { }); } + /** + * Initialize starters before starting the first battle + * @param starters {@linkcode Pokemon} with which to start the first battle + */ initBattle(starters: Starter[]) { const party = this.scene.getParty(); const loadPokemonAssets: Promise[] = []; @@ -564,9 +568,13 @@ export class SelectStarterPhase extends Phase { } const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); - if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { - starterFormIndex = Overrides.STARTER_FORM_OVERRIDE; + if ( + starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES && + starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]] + ) { + starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]; } + let starterGender = starter.species.malePercent !== null ? !starterProps.female ? Gender.MALE : Gender.FEMALE : Gender.GENDERLESS; diff --git a/src/test/abilities/battle_bond.test.ts b/src/test/abilities/battle_bond.test.ts new file mode 100644 index 00000000000..2752ceed896 --- /dev/null +++ b/src/test/abilities/battle_bond.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import * as Overrides from "#app/overrides"; +import { Moves } from "#app/data/enums/moves.js"; +import { Abilities } from "#app/data/enums/abilities.js"; +import { Species } from "#app/data/enums/species.js"; +import { Status, StatusEffect } from "#app/data/status-effect.js"; +import { TurnEndPhase } from "#app/phases.js"; +import { QuietFormChangePhase } from "#app/form-change-phase.js"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - BATTLE BOND", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.SPLASH; + vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BATTLE_BOND); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + test( + "check if fainted pokemon switches to base form on arena reset", + async () => { + const baseForm = 1, + ashForm = 2; + vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ + [Species.GRENINJA]: ashForm, + }); + + await game.startBattle([Species.MAGIKARP, Species.GRENINJA]); + + const greninja = game.scene.getParty().find((p) => p.species.speciesId === Species.GRENINJA); + expect(greninja).not.toBe(undefined); + expect(greninja.formIndex).toBe(ashForm); + + greninja.hp = 0; + greninja.status = new Status(StatusEffect.FAINT); + expect(greninja.isFainted()).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(QuietFormChangePhase); + + expect(greninja.formIndex).toBe(baseForm); + }, + TIMEOUT + ); +}); diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts new file mode 100644 index 00000000000..8b9c730c4c2 --- /dev/null +++ b/src/test/abilities/disguise.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import * as Overrides from "#app/overrides"; +import { Moves } from "#app/data/enums/moves.js"; +import { Abilities } from "#app/data/enums/abilities.js"; +import { Species } from "#app/data/enums/species.js"; +import { Status, StatusEffect } from "#app/data/status-effect.js"; +import { TurnEndPhase } from "#app/phases.js"; +import { QuietFormChangePhase } from "#app/form-change-phase.js"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - DISGUISE", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.SPLASH; + vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DISGUISE); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + test( + "check if fainted pokemon switched to base form on arena reset", + async () => { + const baseForm = 0, + bustedForm = 1; + vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ + [Species.MIMIKYU]: bustedForm, + }); + + await game.startBattle([Species.MAGIKARP, Species.MIMIKYU]); + + const mimikyu = game.scene.getParty().find((p) => p.species.speciesId === Species.MIMIKYU); + expect(mimikyu).not.toBe(undefined); + expect(mimikyu.formIndex).toBe(bustedForm); + + mimikyu.hp = 0; + mimikyu.status = new Status(StatusEffect.FAINT); + expect(mimikyu.isFainted()).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(QuietFormChangePhase); + + expect(mimikyu.formIndex).toBe(baseForm); + }, + TIMEOUT + ); +}); diff --git a/src/test/abilities/power_construct.test.ts b/src/test/abilities/power_construct.test.ts new file mode 100644 index 00000000000..c568ba9db3e --- /dev/null +++ b/src/test/abilities/power_construct.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import * as Overrides from "#app/overrides"; +import { Moves } from "#app/data/enums/moves.js"; +import { Abilities } from "#app/data/enums/abilities.js"; +import { Species } from "#app/data/enums/species.js"; +import { Status, StatusEffect } from "#app/data/status-effect.js"; +import { TurnEndPhase } from "#app/phases.js"; +import { QuietFormChangePhase } from "#app/form-change-phase.js"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - POWER CONSTRUCT", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.SPLASH; + vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.POWER_CONSTRUCT); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + test( + "check if fainted pokemon switches to base form on arena reset", + async () => { + const baseForm = 2, + completeForm = 4; + vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ + [Species.ZYGARDE]: completeForm, + }); + + await game.startBattle([Species.MAGIKARP, Species.ZYGARDE]); + + const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE); + expect(zygarde).not.toBe(undefined); + expect(zygarde.formIndex).toBe(completeForm); + + zygarde.hp = 0; + zygarde.status = new Status(StatusEffect.FAINT); + expect(zygarde.isFainted()).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(QuietFormChangePhase); + + expect(zygarde.formIndex).toBe(baseForm); + }, + TIMEOUT + ); +}); diff --git a/src/test/abilities/schooling.test.ts b/src/test/abilities/schooling.test.ts new file mode 100644 index 00000000000..35a05cff87a --- /dev/null +++ b/src/test/abilities/schooling.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import * as Overrides from "#app/overrides"; +import { Moves } from "#app/data/enums/moves.js"; +import { Abilities } from "#app/data/enums/abilities.js"; +import { Species } from "#app/data/enums/species.js"; +import { Status, StatusEffect } from "#app/data/status-effect.js"; +import { TurnEndPhase } from "#app/phases.js"; +import { QuietFormChangePhase } from "#app/form-change-phase.js"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - SCHOOLING", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.SPLASH; + vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SCHOOLING); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + test( + "check if fainted pokemon switches to base form on arena reset", + async () => { + const soloForm = 0, + schoolForm = 1; + vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ + [Species.WISHIWASHI]: schoolForm, + }); + + await game.startBattle([Species.MAGIKARP, Species.WISHIWASHI]); + + const wishiwashi = game.scene.getParty().find((p) => p.species.speciesId === Species.WISHIWASHI); + expect(wishiwashi).not.toBe(undefined); + expect(wishiwashi.formIndex).toBe(schoolForm); + + wishiwashi.hp = 0; + wishiwashi.status = new Status(StatusEffect.FAINT); + expect(wishiwashi.isFainted()).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(QuietFormChangePhase); + + expect(wishiwashi.formIndex).toBe(soloForm); + }, + TIMEOUT + ); +}); diff --git a/src/test/abilities/shields_down.test.ts b/src/test/abilities/shields_down.test.ts new file mode 100644 index 00000000000..6e7cb77e079 --- /dev/null +++ b/src/test/abilities/shields_down.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import * as Overrides from "#app/overrides"; +import { Moves } from "#app/data/enums/moves.js"; +import { Abilities } from "#app/data/enums/abilities.js"; +import { Species } from "#app/data/enums/species.js"; +import { Status, StatusEffect } from "#app/data/status-effect.js"; +import { TurnEndPhase } from "#app/phases.js"; +import { QuietFormChangePhase } from "#app/form-change-phase.js"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - SHIELDS DOWN", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.SPLASH; + vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SHIELDS_DOWN); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + test( + "check if fainted pokemon switched to base form on arena reset", + async () => { + const meteorForm = 0, + coreForm = 7; + vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ + [Species.MINIOR]: coreForm, + }); + + await game.startBattle([Species.MAGIKARP, Species.MINIOR]); + + const minior = game.scene.getParty().find((p) => p.species.speciesId === Species.MINIOR); + expect(minior).not.toBe(undefined); + expect(minior.formIndex).toBe(coreForm); + + minior.hp = 0; + minior.status = new Status(StatusEffect.FAINT); + expect(minior.isFainted()).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(QuietFormChangePhase); + + expect(minior.formIndex).toBe(meteorForm); + }, + TIMEOUT + ); +}); diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index 76bc3231f8a..697350dc271 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -1,9 +1,9 @@ -import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import Phaser from "phaser"; import GameManager from "#app/test/utils/gameManager"; -import * as overrides from "#app/overrides"; -import {Abilities} from "#app/data/enums/abilities"; -import {Species} from "#app/data/enums/species"; +import * as Overrides from "#app/overrides"; +import { Abilities } from "#app/data/enums/abilities"; +import { Species } from "#app/data/enums/species"; import { CommandPhase, DamagePhase, @@ -12,18 +12,21 @@ import { PostSummonPhase, SwitchPhase, SwitchSummonPhase, - TurnEndPhase, TurnInitPhase, + TurnEndPhase, + TurnInitPhase, TurnStartPhase, } from "#app/phases"; -import {Mode} from "#app/ui/ui"; -import {Stat} from "#app/data/pokemon-stat"; -import {Moves} from "#app/data/enums/moves"; -import {getMovePosition} from "#app/test/utils/gameManagerUtils"; -import {Command} from "#app/ui/command-ui-handler"; -import {QuietFormChangePhase} from "#app/form-change-phase"; +import { Mode } from "#app/ui/ui"; +import { Stat } from "#app/data/pokemon-stat"; +import { Moves } from "#app/data/enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { Command } from "#app/ui/command-ui-handler"; +import { QuietFormChangePhase } from "#app/form-change-phase"; +import { Status, StatusEffect } from "#app/data/status-effect.js"; +const TIMEOUT = 20 * 1000; -describe("Abilities - Zen mode", () => { +describe("Abilities - ZEN MODE", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -40,103 +43,139 @@ describe("Abilities - Zen mode", () => { beforeEach(() => { game = new GameManager(phaserGame); const moveToUse = Moves.SPLASH; - vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); - vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); - vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ZEN_MODE); - vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ZEN_MODE); + vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); }); - it("ZEN MODE - not enough damage to change form", async() => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ - Species.DARMANITAN, - ]); - game.scene.getParty()[0].stats[Stat.SPD] = 1; - game.scene.getParty()[0].stats[Stat.HP] = 100; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + test( + "not enough damage to change form", + async () => { + const moveToUse = Moves.SPLASH; + await game.startBattle([Species.DARMANITAN]); + game.scene.getParty()[0].stats[Stat.SPD] = 1; + game.scene.getParty()[0].stats[Stat.HP] = 100; + game.scene.getParty()[0].hp = 100; + expect(game.scene.getParty()[0].formIndex).toBe(0); - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, moveToUse); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase, false); - // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); - const damagePhase = game.scene.getCurrentPhase() as DamagePhase; - damagePhase.updateAmount(40); - await game.phaseInterceptor.runFrom(DamagePhase).to(TurnEndPhase, false); - expect(game.scene.getParty()[0].hp).toBeLessThan(100); - expect(game.scene.getParty()[0].formIndex).toBe(0); - }, 20000); + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase, false); + // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); + const damagePhase = game.scene.getCurrentPhase() as DamagePhase; + damagePhase.updateAmount(40); + await game.phaseInterceptor.runFrom(DamagePhase).to(TurnEndPhase, false); + expect(game.scene.getParty()[0].hp).toBeLessThan(100); + expect(game.scene.getParty()[0].formIndex).toBe(0); + }, + TIMEOUT + ); - it("ZEN MODE - enough damage to change form", async() => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ - Species.DARMANITAN, - ]); - game.scene.getParty()[0].stats[Stat.SPD] = 1; - game.scene.getParty()[0].stats[Stat.HP] = 1000; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + test( + "enough damage to change form", + async () => { + const moveToUse = Moves.SPLASH; + await game.startBattle([Species.DARMANITAN]); + game.scene.getParty()[0].stats[Stat.SPD] = 1; + game.scene.getParty()[0].stats[Stat.HP] = 1000; + game.scene.getParty()[0].hp = 100; + expect(game.scene.getParty()[0].formIndex).toBe(0); - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, moveToUse); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(QuietFormChangePhase); - await game.phaseInterceptor.to(TurnInitPhase, false); - expect(game.scene.getParty()[0].hp).not.toBe(100); - expect(game.scene.getParty()[0].formIndex).not.toBe(0); - }, 20000); + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(QuietFormChangePhase); + await game.phaseInterceptor.to(TurnInitPhase, false); + expect(game.scene.getParty()[0].hp).not.toBe(100); + expect(game.scene.getParty()[0].formIndex).not.toBe(0); + }, + TIMEOUT + ); - it("ZEN MODE - kill pokemon while on zen mode", async() => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ - Species.DARMANITAN, - Species.CHARIZARD, - ]); - game.scene.getParty()[0].stats[Stat.SPD] = 1; - game.scene.getParty()[0].stats[Stat.HP] = 1000; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + test( + "kill pokemon while on zen mode", + async () => { + const moveToUse = Moves.SPLASH; + await game.startBattle([Species.DARMANITAN, Species.CHARIZARD]); + game.scene.getParty()[0].stats[Stat.SPD] = 1; + game.scene.getParty()[0].stats[Stat.HP] = 1000; + game.scene.getParty()[0].hp = 100; + expect(game.scene.getParty()[0].formIndex).toBe(0); - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, moveToUse); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase, false); - // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); - const damagePhase = game.scene.getCurrentPhase() as DamagePhase; - damagePhase.updateAmount(80); - await game.phaseInterceptor.runFrom(DamagePhase).to(QuietFormChangePhase); - expect(game.scene.getParty()[0].hp).not.toBe(100); - expect(game.scene.getParty()[0].formIndex).not.toBe(0); - await game.killPokemon(game.scene.getParty()[0]); - expect(game.scene.getParty()[0].isFainted()).toBe(true); - await game.phaseInterceptor.run(MessagePhase); - await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.run(TurnStartPhase); - game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { - game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, 0, 1, false, false)); - game.scene.ui.setMode(Mode.MESSAGE); - }); - game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { - game.endPhase(); - }); - await game.phaseInterceptor.run(SwitchPhase); - await game.phaseInterceptor.to(PostSummonPhase); - expect(game.scene.getParty()[1].formIndex).toBe(1); - }, 20000); + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase, false); + // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); + const damagePhase = game.scene.getCurrentPhase() as DamagePhase; + damagePhase.updateAmount(80); + await game.phaseInterceptor.runFrom(DamagePhase).to(QuietFormChangePhase); + expect(game.scene.getParty()[0].hp).not.toBe(100); + expect(game.scene.getParty()[0].formIndex).not.toBe(0); + await game.killPokemon(game.scene.getParty()[0]); + expect(game.scene.getParty()[0].isFainted()).toBe(true); + await game.phaseInterceptor.run(MessagePhase); + await game.phaseInterceptor.run(EnemyCommandPhase); + await game.phaseInterceptor.run(TurnStartPhase); + game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { + game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, 0, 1, false, false)); + game.scene.ui.setMode(Mode.MESSAGE); + }); + game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + await game.phaseInterceptor.run(SwitchPhase); + await game.phaseInterceptor.to(PostSummonPhase); + expect(game.scene.getParty()[1].formIndex).toBe(1); + }, + TIMEOUT + ); + + test( + "check if fainted pokemon switches to base form on arena reset", + async () => { + const baseForm = 0, + zenForm = 1; + vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ + [Species.DARMANITAN]: zenForm, + }); + + await game.startBattle([Species.MAGIKARP, Species.DARMANITAN]); + + const darmanitan = game.scene.getParty().find((p) => p.species.speciesId === Species.DARMANITAN); + expect(darmanitan).not.toBe(undefined); + expect(darmanitan.formIndex).toBe(zenForm); + + darmanitan.hp = 0; + darmanitan.status = new Status(StatusEffect.FAINT); + expect(darmanitan.isFainted()).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(QuietFormChangePhase); + + expect(darmanitan.formIndex).toBe(baseForm); + }, + TIMEOUT + ); }); diff --git a/src/test/abilities/zero_to_hero.test.ts b/src/test/abilities/zero_to_hero.test.ts new file mode 100644 index 00000000000..a282be2750c --- /dev/null +++ b/src/test/abilities/zero_to_hero.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import * as Overrides from "#app/overrides"; +import { Moves } from "#app/data/enums/moves.js"; +import { Abilities } from "#app/data/enums/abilities.js"; +import { Species } from "#app/data/enums/species.js"; +import { Status, StatusEffect } from "#app/data/status-effect.js"; +import { TurnEndPhase } from "#app/phases.js"; +import { QuietFormChangePhase } from "#app/form-change-phase.js"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - ZERO TO HERO", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.SPLASH; + vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ZERO_TO_HERO); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + test( + "check if fainted pokemon switches to base form on arena reset", + async () => { + const baseForm = 0, + heroForm = 1; + vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ + [Species.PALAFIN]: heroForm, + }); + + await game.startBattle([Species.MAGIKARP, Species.PALAFIN]); + + const palafin = game.scene.getParty().find((p) => p.species.speciesId === Species.PALAFIN); + expect(palafin).not.toBe(undefined); + expect(palafin.formIndex).toBe(heroForm); + + palafin.hp = 0; + palafin.status = new Status(StatusEffect.FAINT); + expect(palafin.isFainted()).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(QuietFormChangePhase); + + expect(palafin.formIndex).toBe(baseForm); + }, + TIMEOUT + ); +}); diff --git a/tsconfig.json b/tsconfig.json index 3e8f300dd0c..ec0b5e95ac2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "baseUrl": "./src", "paths": { "#app/*": ["*.ts"], - "#app": ["."] + "#app": ["."], + "#test/*": ["./test/*.ts"] }, "outDir": "./build", "noEmit": true