diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 8d13e4a0de3..6346a7c1d26 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -3976,7 +3976,7 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr { /** * Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander}. * When the source of an ability with this attribute detects a Dondozo as their active ally, the source "jumps - * into the Dondozo's mouth"m sharply boosting the Dondozo's stats, cancelling the source's moves, and + * into the Dondozo's mouth", sharply boosting the Dondozo's stats, cancelling the source's moves, and * causing attacks that target the source to always miss. */ export class CommanderAbAttr extends AbAttr { diff --git a/test/abilities/imposter.test.ts b/test/abilities/imposter.test.ts deleted file mode 100644 index 30491139877..00000000000 --- a/test/abilities/imposter.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import Phaser from "phaser"; -import GameManager from "#test/testUtils/gameManager"; -import { SpeciesId } from "#enums/species-id"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { MoveId } from "#enums/move-id"; -import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; -import { AbilityId } from "#enums/ability-id"; - -// TODO: Add more tests once Imposter is fully implemented -describe("Abilities - Imposter", () => { - 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 - .battleStyle("single") - .enemySpecies(SpeciesId.MEW) - .enemyLevel(200) - .enemyAbility(AbilityId.BEAST_BOOST) - .enemyPassiveAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH) - .ability(AbilityId.IMPOSTER) - .moveset(MoveId.SPLASH); - }); - - it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { - await game.classicMode.startBattle([SpeciesId.DITTO]); - - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to(TurnEndPhase); - - const player = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; - - expect(player.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); - expect(player.getAbility()).toBe(enemy.getAbility()); - expect(player.getGender()).toBe(enemy.getGender()); - - expect(player.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); - for (const s of EFFECTIVE_STATS) { - expect(player.getStat(s, false)).toBe(enemy.getStat(s, false)); - } - - for (const s of BATTLE_STATS) { - expect(player.getStatStage(s)).toBe(enemy.getStatStage(s)); - } - - const playerMoveset = player.getMoveset(); - const enemyMoveset = player.getMoveset(); - - expect(playerMoveset.length).toBe(enemyMoveset.length); - for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { - expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId); - } - - const playerTypes = player.getTypes(); - const enemyTypes = enemy.getTypes(); - - expect(playerTypes.length).toBe(enemyTypes.length); - for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) { - expect(playerTypes[i]).toBe(enemyTypes[i]); - } - }); - - it("should copy in-battle overridden stats", async () => { - game.override.enemyMoveset([MoveId.POWER_SPLIT]); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - - const player = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; - - const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2); - const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2); - - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(player.getStat(Stat.ATK, false)).toBe(avgAtk); - expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk); - - expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); - expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); - }); - - it("should set each move's pp to a maximum of 5", async () => { - game.override.enemyMoveset([MoveId.SWORDS_DANCE, MoveId.GROWL, MoveId.SKETCH, MoveId.RECOVER]); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - const player = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnEndPhase); - - player.getMoveset().forEach(move => { - // Should set correct maximum PP without touching `ppUp` - if (move) { - if (move.moveId === MoveId.SKETCH) { - expect(move.getMovePp()).toBe(1); - } else { - expect(move.getMovePp()).toBe(5); - } - expect(move.ppUp).toBe(0); - } - }); - }); - - it("should activate its ability if it copies one that activates on summon", async () => { - game.override.enemyAbility(AbilityId.INTIMIDATE); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to("MoveEndPhase"); - - expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); - }); - - it("should persist transformed attributes across reloads", async () => { - game.override.moveset([MoveId.ABSORB]); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - - const player = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; - - game.move.select(MoveId.SPLASH); - await game.doKillOpponents(); - await game.toNextWave(); - - expect(game.scene.phaseManager.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); - expect(game.scene.currentBattle.waveIndex).toBe(2); - - await game.reload.reloadSession(); - - const playerReloaded = game.scene.getPlayerPokemon()!; - const playerMoveset = player.getMoveset(); - - expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); - expect(playerReloaded.getAbility()).toBe(enemy.getAbility()); - expect(playerReloaded.getGender()).toBe(enemy.getGender()); - - expect(playerReloaded.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); - for (const s of EFFECTIVE_STATS) { - expect(playerReloaded.getStat(s, false)).toBe(enemy.getStat(s, false)); - } - - expect(playerMoveset.length).toEqual(1); - expect(playerMoveset[0]?.moveId).toEqual(MoveId.SPLASH); - }); - - it("should stay transformed with the correct form after reload", async () => { - game.override.moveset([MoveId.ABSORB]).enemySpecies(SpeciesId.UNOWN); - await game.classicMode.startBattle([SpeciesId.DITTO]); - - const enemy = game.scene.getEnemyPokemon()!; - - // change form - enemy.species.forms[5]; - enemy.species.formIndex = 5; - - game.move.select(MoveId.SPLASH); - await game.doKillOpponents(); - await game.toNextWave(); - - expect(game.scene.phaseManager.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); - expect(game.scene.currentBattle.waveIndex).toBe(2); - - await game.reload.reloadSession(); - - const playerReloaded = game.scene.getPlayerPokemon()!; - - expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); - expect(playerReloaded.getSpeciesForm().formIndex).toBe(enemy.getSpeciesForm().formIndex); - }); -}); diff --git a/test/moves/transform-imposter.test.ts b/test/moves/transform-imposter.test.ts new file mode 100644 index 00000000000..11fb92ec97c --- /dev/null +++ b/test/moves/transform-imposter.test.ts @@ -0,0 +1,379 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#test/testUtils/gameManager"; +import { SpeciesId } from "#enums/species-id"; +import { MoveId } from "#enums/move-id"; +import { Stat } from "#enums/stat"; +import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; +import { StatusEffect } from "#enums/status-effect"; +import { MoveResult } from "#enums/move-result"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Status } from "#app/data/status-effect"; +import { PokemonType } from "#enums/pokemon-type"; +import { BerryType } from "#enums/berry-type"; +import type { EnemyPokemon } from "#app/field/pokemon"; +import Pokemon from "#app/field/pokemon"; +import { BattleType } from "#enums/battle-type"; + +// TODO: Add more tests once Transform/Imposter are fully implemented +describe("Transforming Effects", () => { + 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 + .battleStyle("single") + .enemySpecies(SpeciesId.MEW) + .enemyLevel(200) + .enemyAbility(AbilityId.BEAST_BOOST) + .enemyPassiveAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .ability(AbilityId.STURDY); + }); + + // Contains logic shared by both Transform and Impostor (for brevity) + describe("Phases - PokemonTransformPhase", async () => { + it("should copy target's species, ability, gender, all stats except HP, all stat stages, moveset and types", async () => { + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const ditto = game.field.getPlayerPokemon(); + const mew = game.field.getEnemyPokemon(); + + mew.setStatStage(Stat.ATK, 4); + + game.move.use(MoveId.SPLASH); + game.scene.phaseManager.unshiftNew("PokemonTransformPhase", ditto.getBattlerIndex(), mew.getBattlerIndex()); + await game.toEndOfTurn(); + + expect(ditto.isTransformed()).toBe(true); + expect(ditto.getSpeciesForm().speciesId).toBe(mew.getSpeciesForm().speciesId); + expect(ditto.getAbility()).toBe(mew.getAbility()); + expect(ditto.getGender()).toBe(mew.getGender()); + + const playerStats = ditto.getStats(false); + const enemyStats = mew.getStats(false); + // HP stays the same; all other stats should carry over + expect(playerStats[0]).not.toBe(enemyStats[0]); + expect(playerStats.slice(1)).toEqual(enemyStats.slice(1)); + + // Stat stages/moveset IDs + expect(ditto.getStatStages()).toEqual(mew.getStatStages()); + + expect(ditto.getMoveset().map(m => m.moveId)).toEqual(ditto.getMoveset().map(m => m.moveId)); + + expect(ditto.getTypes()).toEqual(mew.getTypes()); + }); + + // TODO: This is not implemented + it.todo("should copy the target's original typing if target is typeless", async () => { + game.override.enemySpecies(SpeciesId.MAGMAR); + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const ditto = game.field.getPlayerPokemon(); + const magmar = game.field.getEnemyPokemon(); + + game.move.use(MoveId.TRANSFORM); + await game.move.forceEnemyMove(MoveId.BURN_UP); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toEndOfTurn(); + + expect(magmar.getTypes()).toEqual([PokemonType.UNKNOWN]); + expect(ditto.getTypes()).toEqual([PokemonType.FIRE]); + }); + + it("should not consider the target's Tera Type when copying types", async () => { + game.override.enemySpecies(SpeciesId.MAGMAR); + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const ditto = game.field.getPlayerPokemon(); + const magmar = game.field.getEnemyPokemon(); + magmar.isTerastallized = true; + magmar.teraType = PokemonType.DARK; + + game.move.use(MoveId.TRANSFORM); + await game.toEndOfTurn(); + + expect(ditto.getTypes(true)).toEqual([PokemonType.FIRE]); + }); + + // TODO: This is not currently implemented + it.todo("should copy volatile status effects", async () => { + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const ditto = game.field.getPlayerPokemon(); + const mew = game.field.getEnemyPokemon(); + mew.addTag(BattlerTagType.SEEDED, 0, MoveId.LEECH_SEED, ditto.id); + mew.addTag(BattlerTagType.CONFUSED, 4, MoveId.AXE_KICK, ditto.id); + + game.move.use(MoveId.TRANSFORM); + await game.toEndOfTurn(); + + expect(ditto.getTag(BattlerTagType.SEEDED)).toBeDefined(); + expect(ditto.getTag(BattlerTagType.CONFUSED)).toBeDefined(); + }); + + // TODO: This is not implemented + it.todo("should copy the target's rage fist hit count"); + + it("should not copy friendship, held items, nickname, level or non-volatile status effects", async () => { + game.override.enemyHeldItems([{ name: "BERRY", count: 1, type: BerryType.SITRUS }]); + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const ditto = game.field.getPlayerPokemon(); + const mew = game.field.getEnemyPokemon(); + + mew.status = new Status(StatusEffect.POISON); + mew.friendship = 255; + mew.nickname = btoa(unescape(encodeURIComponent("Pink Furry Cat Thing"))); + + game.move.use(MoveId.TRANSFORM); + await game.toEndOfTurn(); + + expect(ditto.status?.effect).toBeUndefined(); + expect(ditto.getNameToRender()).not.toBe(mew.getNameToRender()); + expect(ditto.level).not.toBe(mew.level); + expect(ditto.friendship).not.toBe(mew.friendship); + expect(ditto.getHeldItems()).not.toEqual(mew.getHeldItems()); + }); + + it("should copy in-battle overridden stats", async () => { + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const player = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); + + const oldAtk = player.getStat(Stat.ATK); + const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2); + + game.move.use(MoveId.TRANSFORM); + await game.move.forceEnemyMove(MoveId.POWER_SPLIT); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toEndOfTurn(); + + expect(player.getStat(Stat.ATK, false)).toBe(avgAtk); + expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk); + expect(avgAtk).not.toBe(oldAtk); + }); + + it("should set each move's pp to a maximum of 5 without affecting PP ups", async () => { + game.override.enemyMoveset([MoveId.SWORDS_DANCE, MoveId.GROWL, MoveId.SKETCH, MoveId.RECOVER]); + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const player = game.field.getPlayerPokemon(); + + game.move.use(MoveId.TRANSFORM); + await game.toEndOfTurn(); + + player.getMoveset().forEach(move => { + // Should set correct maximum PP without touching `ppUp` + if (move) { + if (move.moveId === MoveId.SKETCH) { + expect(move.getMovePp()).toBe(1); + } else { + expect(move.getMovePp()).toBe(5); + } + expect(move.ppUp).toBe(0); + } + }); + }); + + it("should activate its ability if it copies one that activates on summon", async () => { + game.override.enemyAbility(AbilityId.INTIMIDATE); + await game.classicMode.startBattle([SpeciesId.DITTO]); + + game.move.use(MoveId.TRANSFORM); + game.phaseInterceptor.clearLogs(); + await game.toEndOfTurn(); + + expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(-1); + expect(game.phaseInterceptor.log).toContain("StatStageChangePhase"); + }); + + it("should persist transformed attributes across reloads", async () => { + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const player = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); + + game.move.use(MoveId.TRANSFORM); + await game.move.forceEnemyMove(MoveId.MEMENTO); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextWave(); + + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + expect(game.scene.currentBattle.waveIndex).toBe(2); + + await game.reload.reloadSession(); + + const playerReloaded = game.field.getPlayerPokemon(); + const playerMoveset = player.getMoveset(); + + expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(playerReloaded.getAbility()).toBe(enemy.getAbility()); + expect(playerReloaded.getGender()).toBe(enemy.getGender()); + + expect(playerMoveset.map(m => m.moveId)).toEqual([MoveId.MEMENTO]); + }); + + it("should stay transformed with the correct form after reload", async () => { + game.override.enemySpecies(SpeciesId.DARMANITAN); + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const player = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); + + // change form + enemy.species.formIndex = 1; + + game.move.use(MoveId.TRANSFORM); + await game.move.forceEnemyMove(MoveId.MEMENTO); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextWave(); + + expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); + expect(game.scene.currentBattle.waveIndex).toBe(2); + + expect(player.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(player.getSpeciesForm().formIndex).toBe(enemy.getSpeciesForm().formIndex); + + await game.reload.reloadSession(); + + const playerReloaded = game.field.getPlayerPokemon(); + expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(playerReloaded.getSpeciesForm().formIndex).toBe(enemy.getSpeciesForm().formIndex); + }); + }); + + describe("Moves - Transform", () => { + it.each<{ cause: string; callback: (p: Pokemon) => void; player?: boolean }>([ + { + cause: "user is fused", + callback: p => vi.spyOn(p, "isFusion").mockReturnValue(true), + }, + { + cause: "target is fused", + callback: p => vi.spyOn(p, "isFusion").mockReturnValue(true), + player: false, + }, + { + cause: "user is transformed", + callback: p => vi.spyOn(p, "isTransformed").mockReturnValue(true), + }, + { + cause: "target is transformed", + callback: p => vi.spyOn(p, "isTransformed").mockReturnValue(true), + player: false, + }, + { + cause: "user has illusion", + callback: p => p.setIllusion(game.scene.getEnemyParty()[1]), + }, + { + cause: "target has illusion", + callback: p => p.setIllusion(game.scene.getEnemyParty()[1]), + player: false, + }, + { + cause: "target is behind a substitute", + callback: p => p.addTag(BattlerTagType.SUBSTITUTE, 1, MoveId.SUBSTITUTE, p.id), + player: false, + }, + ])("should fail if $cause", async ({ callback, player = true }) => { + game.override.battleType(BattleType.TRAINER); // ensures 2 enemy pokemon for illusion + await game.classicMode.startBattle([SpeciesId.DITTO, SpeciesId.ABOMASNOW]); + + callback(player ? game.field.getPlayerPokemon() : game.field.getEnemyPokemon()); + + game.move.use(MoveId.TRANSFORM); + await game.toEndOfTurn(); + + const ditto = game.field.getPlayerPokemon(); + expect(ditto.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(game.phaseInterceptor.log).not.toContain("PokemonTransformPhase"); + }); + }); + + describe("Abilities - Imposter", () => { + beforeEach(async () => { + game.override.ability(AbilityId.NONE); + // Mock ability index to always be HA (ensuring Ditto has Imposter and nobody else). + ( + vi.spyOn(Pokemon.prototype as any, "generateAbilityIndex") as MockInstance< + (typeof Pokemon.prototype)["generateAbilityIndex"] + > + ).mockReturnValue(3); + }); + + it.each<{ name: string; callback: (p: EnemyPokemon) => void }>([ + { + name: "opponents with substitutes", + callback: p => p.addTag(BattlerTagType.SUBSTITUTE, 1, MoveId.SUBSTITUTE, p.id), + }, + { name: "fused opponents", callback: p => vi.spyOn(p, "isFusion").mockReturnValue(true) }, + { + name: "opponents with illusions", + callback: p => p.setIllusion(game.scene.getEnemyParty()[1]), // doesn't really matter what the illusion is, merely that it exists + }, + ])("should ignore $name during target selection", async ({ callback }) => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.GYARADOS, SpeciesId.MILOTIC, SpeciesId.DITTO]); + + const ditto = game.scene.getPlayerParty()[2]; + + const [enemy1, enemy2] = game.scene.getEnemyField(); + // Override enemy 1 to be a fusion/illusion + callback(enemy1); + + expect(ditto.canTransformInto(enemy1)).toBe(false); + expect(ditto.canTransformInto(enemy2)).toBe(true); + + // Switch out to Ditto + game.doSwitchPokemon(2); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.toEndOfTurn(); + + expect(ditto.isActive()).toBe(true); + expect(ditto.isTransformed()).toBe(true); + expect(ditto.getSpeciesForm().speciesId).toBe(enemy2.getSpeciesForm().speciesId); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + expect(game.phaseInterceptor.log).toContain("PokemonTransformPhase"); + }); + + it("should not activate if both opponents are fused or have illusions", async () => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.GYARADOS, SpeciesId.MILOTIC, SpeciesId.DITTO]); + + const [gyarados, , ditto] = game.scene.getPlayerParty(); + const [enemy1, enemy2] = game.scene.getEnemyParty(); + // Override enemy 1 to be a fusion & enemy 2 to have illusion + vi.spyOn(enemy1, "isFusion").mockReturnValue(true); + enemy2.setIllusion(gyarados); + + expect(ditto.canTransformInto(enemy1)).toBe(false); + expect(ditto.canTransformInto(enemy2)).toBe(false); + + // Switch out to Ditto + game.doSwitchPokemon(2); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.toEndOfTurn(); + + expect(ditto.isActive()).toBe(true); + expect(ditto.isTransformed()).toBe(false); + expect(ditto.getSpeciesForm().speciesId).toBe(SpeciesId.DITTO); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + expect(game.phaseInterceptor.log).not.toContain("PokemonTransformPhase"); + }); + }); +}); diff --git a/test/moves/transform.test.ts b/test/moves/transform.test.ts deleted file mode 100644 index 4fbaf0136ab..00000000000 --- a/test/moves/transform.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import Phaser from "phaser"; -import GameManager from "#test/testUtils/gameManager"; -import { SpeciesId } from "#enums/species-id"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { MoveId } from "#enums/move-id"; -import { Stat, EFFECTIVE_STATS } from "#enums/stat"; -import { AbilityId } from "#enums/ability-id"; -import { BattlerIndex } from "#enums/battler-index"; - -// TODO: Add more tests once Transform is fully implemented -describe("Moves - Transform", () => { - 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 - .battleStyle("single") - .enemySpecies(SpeciesId.MEW) - .enemyLevel(200) - .enemyAbility(AbilityId.BEAST_BOOST) - .enemyPassiveAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH) - .ability(AbilityId.INTIMIDATE) - .moveset([MoveId.TRANSFORM]); - }); - - it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { - await game.classicMode.startBattle([SpeciesId.DITTO]); - - game.move.select(MoveId.TRANSFORM); - await game.phaseInterceptor.to(TurnEndPhase); - - const player = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; - - expect(player.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); - expect(player.getAbility()).toBe(enemy.getAbility()); - expect(player.getGender()).toBe(enemy.getGender()); - - // copies all stats except hp - expect(player.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); - for (const s of EFFECTIVE_STATS) { - expect(player.getStat(s, false)).toBe(enemy.getStat(s, false)); - } - - expect(player.getStatStages()).toEqual(enemy.getStatStages()); - - // move IDs are equal - expect(player.getMoveset().map(m => m.moveId)).toEqual(enemy.getMoveset().map(m => m.moveId)); - - expect(player.getTypes()).toEqual(enemy.getTypes()); - }); - - it("should copy in-battle overridden stats", async () => { - game.override.enemyMoveset([MoveId.POWER_SPLIT]); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - - const player = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; - - const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2); - const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2); - - game.move.select(MoveId.TRANSFORM); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(player.getStat(Stat.ATK, false)).toBe(avgAtk); - expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk); - - expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); - expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); - }); - - it("should set each move's pp to a maximum of 5", async () => { - game.override.enemyMoveset([MoveId.SWORDS_DANCE, MoveId.GROWL, MoveId.SKETCH, MoveId.RECOVER]); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - const player = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.TRANSFORM); - await game.phaseInterceptor.to(TurnEndPhase); - - player.getMoveset().forEach(move => { - // Should set correct maximum PP without touching `ppUp` - if (move) { - if (move.moveId === MoveId.SKETCH) { - expect(move.getMovePp()).toBe(1); - } else { - expect(move.getMovePp()).toBe(5); - } - expect(move.ppUp).toBe(0); - } - }); - }); - - it("should activate its ability if it copies one that activates on summon", async () => { - game.override.enemyAbility(AbilityId.INTIMIDATE).ability(AbilityId.BALL_FETCH); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - game.move.select(MoveId.TRANSFORM); - - await game.phaseInterceptor.to("BerryPhase"); - - expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); - }); - - it("should persist transformed attributes across reloads", async () => { - game.override.enemyMoveset([]).moveset([]); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - - const player = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; - - game.move.changeMoveset(player, MoveId.TRANSFORM); - game.move.changeMoveset(enemy, MoveId.MEMENTO); - - game.move.select(MoveId.TRANSFORM); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.toNextWave(); - - expect(game.scene.phaseManager.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); - expect(game.scene.currentBattle.waveIndex).toBe(2); - - await game.reload.reloadSession(); - - const playerReloaded = game.scene.getPlayerPokemon()!; - const playerMoveset = player.getMoveset(); - - expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); - expect(playerReloaded.getAbility()).toBe(enemy.getAbility()); - expect(playerReloaded.getGender()).toBe(enemy.getGender()); - - expect(playerReloaded.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); - for (const s of EFFECTIVE_STATS) { - expect(playerReloaded.getStat(s, false)).toBe(enemy.getStat(s, false)); - } - - expect(playerMoveset.length).toEqual(1); - expect(playerMoveset[0]?.moveId).toEqual(MoveId.MEMENTO); - }); - - it("should stay transformed with the correct form after reload", async () => { - game.override.enemyMoveset([]).moveset([]).enemySpecies(SpeciesId.DARMANITAN); - - await game.classicMode.startBattle([SpeciesId.DITTO]); - - const player = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; - - // change form - enemy.species.forms[1]; - enemy.species.formIndex = 1; - - game.move.changeMoveset(player, MoveId.TRANSFORM); - game.move.changeMoveset(enemy, MoveId.MEMENTO); - - game.move.select(MoveId.TRANSFORM); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.toNextWave(); - - expect(game.scene.phaseManager.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); - expect(game.scene.currentBattle.waveIndex).toBe(2); - - await game.reload.reloadSession(); - - const playerReloaded = game.scene.getPlayerPokemon()!; - - expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); - expect(playerReloaded.getSpeciesForm().formIndex).toBe(enemy.getSpeciesForm().formIndex); - }); -}); diff --git a/test/testUtils/phaseInterceptor.ts b/test/testUtils/phaseInterceptor.ts index 9d046fc85ba..415ef02153c 100644 --- a/test/testUtils/phaseInterceptor.ts +++ b/test/testUtils/phaseInterceptor.ts @@ -64,6 +64,7 @@ import { PostGameOverPhase } from "#app/phases/post-game-over-phase"; import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase"; import type { PhaseClass, PhaseString } from "#app/@types/phase-types"; +import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase"; export interface PromptHandler { phaseTarget?: string; @@ -142,6 +143,7 @@ export default class PhaseInterceptor { [LevelCapPhase, this.startPhase], [AttemptRunPhase, this.startPhase], [SelectBiomePhase, this.startPhase], + [PokemonTransformPhase, this.startPhase], [MysteryEncounterPhase, this.startPhase], [MysteryEncounterOptionSelectedPhase, this.startPhase], [MysteryEncounterBattlePhase, this.startPhase],