Condensed tests into 1 file + added more automated tests

This includes a couple regression tests for the fusion shenanigans
This commit is contained in:
Bertie690 2025-06-21 13:48:50 -04:00
parent d9d119aada
commit 4b8026502d
5 changed files with 382 additions and 374 deletions

View File

@ -3976,7 +3976,7 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
/** /**
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander}. * 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 * 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. * causing attacks that target the source to always miss.
*/ */
export class CommanderAbAttr extends AbAttr { export class CommanderAbAttr extends AbAttr {

View File

@ -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);
});
});

View File

@ -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");
});
});
});

View File

@ -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);
});
});

View File

@ -64,6 +64,7 @@ import { PostGameOverPhase } from "#app/phases/post-game-over-phase";
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase"; import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
import type { PhaseClass, PhaseString } from "#app/@types/phase-types"; import type { PhaseClass, PhaseString } from "#app/@types/phase-types";
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase";
export interface PromptHandler { export interface PromptHandler {
phaseTarget?: string; phaseTarget?: string;
@ -142,6 +143,7 @@ export default class PhaseInterceptor {
[LevelCapPhase, this.startPhase], [LevelCapPhase, this.startPhase],
[AttemptRunPhase, this.startPhase], [AttemptRunPhase, this.startPhase],
[SelectBiomePhase, this.startPhase], [SelectBiomePhase, this.startPhase],
[PokemonTransformPhase, this.startPhase],
[MysteryEncounterPhase, this.startPhase], [MysteryEncounterPhase, this.startPhase],
[MysteryEncounterOptionSelectedPhase, this.startPhase], [MysteryEncounterOptionSelectedPhase, this.startPhase],
[MysteryEncounterBattlePhase, this.startPhase], [MysteryEncounterBattlePhase, this.startPhase],