mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-03 23:12:20 +02:00
Merge pull request #1 from Bertie690/transform-review
This commit is contained in:
commit
3e0e4cda2f
@ -75,7 +75,7 @@ import type {
|
|||||||
AbAttrString,
|
AbAttrString,
|
||||||
AbAttrMap,
|
AbAttrMap,
|
||||||
} from "#app/@types/ability-types";
|
} from "#app/@types/ability-types";
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import type Move from "#app/data/moves/move";
|
import type Move from "#app/data/moves/move";
|
||||||
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
|
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
|
||||||
import type { Constructor } from "#app/utils/common";
|
import type { Constructor } from "#app/utils/common";
|
||||||
@ -3870,60 +3870,39 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
|
|||||||
* Attribute used by {@linkcode AbilityId.IMPOSTER} to transform into a random opposing pokemon on entry.
|
* Attribute used by {@linkcode AbilityId.IMPOSTER} to transform into a random opposing pokemon on entry.
|
||||||
*/
|
*/
|
||||||
export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
||||||
|
private targetIndex: BattlerIndex = BattlerIndex.ATTACKER;
|
||||||
constructor() {
|
constructor() {
|
||||||
super(true, false);
|
super(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTarget(targets: Pokemon[]): Pokemon {
|
/**
|
||||||
let target: Pokemon = targets[0];
|
* Return the correct opponent for Imposter to copy, barring enemies with fusions, substitutes and illusions.
|
||||||
if (targets.length > 1) {
|
* @param user - The {@linkcode Pokemon} with this ability.
|
||||||
globalScene.executeWithSeedOffset(() => {
|
* @returns The {@linkcode Pokemon} to transform into, or `undefined` if none are eligible.
|
||||||
// in a double battle, if one of the opposing pokemon is fused the other one will be chosen
|
* @remarks
|
||||||
// if both are fused, then Imposter will fail below
|
* This sets the private `targetIndex` field to the target's {@linkcode BattlerIndex} on success.
|
||||||
if (targets[0].fusionSpecies) {
|
*/
|
||||||
target = targets[1];
|
private getTarget(user: Pokemon): Pokemon | undefined {
|
||||||
return;
|
// As opposed to the mainline behavior of "always copy the opposite slot",
|
||||||
}
|
// PKR Imposter instead attempts to copy a random eligible opposing Pokemon meeting Transform's criteria.
|
||||||
if (targets[1].fusionSpecies) {
|
// If none are eligible to copy, it will not activate.
|
||||||
target = targets[0];
|
const targets = user.getOpponents().filter(opp => user.canTransformInto(opp));
|
||||||
return;
|
if (targets.length === 0) {
|
||||||
}
|
return undefined;
|
||||||
target = randSeedItem(targets);
|
|
||||||
}, globalScene.currentBattle.waveIndex);
|
|
||||||
} else {
|
|
||||||
target = targets[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
target = target!;
|
const mon = targets[user.randBattleSeedInt(targets.length)];
|
||||||
|
this.targetIndex = mon.getBattlerIndex();
|
||||||
return target;
|
return mon;
|
||||||
}
|
}
|
||||||
|
|
||||||
override canApplyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): boolean {
|
override canApplyPostSummon(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
|
||||||
const targets = pokemon.getOpponents();
|
const target = this.getTarget(pokemon);
|
||||||
const target = this.getTarget(targets);
|
return !!target;
|
||||||
|
|
||||||
if (target?.summonData?.illusion || pokemon?.isTransformed() || target?.isTransformed()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (simulated || !targets.length) {
|
|
||||||
return simulated;
|
|
||||||
}
|
|
||||||
|
|
||||||
// transforming from or into fusion pokemon causes various problems (including crashes and save corruption)
|
|
||||||
return !(this.getTarget(targets).fusionSpecies || pokemon.fusionSpecies);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override applyPostSummon(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): void {
|
override applyPostSummon(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): void {
|
||||||
const target = this.getTarget(pokemon.getOpponents());
|
globalScene.phaseManager.unshiftNew("PokemonTransformPhase", pokemon.getBattlerIndex(), this.targetIndex, true);
|
||||||
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
|
||||||
"PokemonTransformPhase",
|
|
||||||
pokemon.getBattlerIndex(),
|
|
||||||
target.getBattlerIndex(),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3997,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," 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 {
|
||||||
@ -8402,7 +8381,8 @@ export function initAbilities() {
|
|||||||
.bypassFaint(),
|
.bypassFaint(),
|
||||||
new Ability(AbilityId.IMPOSTER, 5)
|
new Ability(AbilityId.IMPOSTER, 5)
|
||||||
.attr(PostSummonTransformAbAttr)
|
.attr(PostSummonTransformAbAttr)
|
||||||
.uncopiable(),
|
.uncopiable()
|
||||||
|
.edgeCase(), // Should copy rage fist hit count, etc (see Transform edge case for full list)
|
||||||
new Ability(AbilityId.INFILTRATOR, 5)
|
new Ability(AbilityId.INFILTRATOR, 5)
|
||||||
.attr(InfiltratorAbAttr)
|
.attr(InfiltratorAbAttr)
|
||||||
.partial(), // does not bypass Mist
|
.partial(), // does not bypass Mist
|
||||||
|
@ -7606,19 +7606,23 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by Transform
|
* Attribute used to transform into the target on move use.
|
||||||
|
*
|
||||||
|
* Used for {@linkcode MoveId.TRANSFORM}.
|
||||||
*/
|
*/
|
||||||
export class TransformAttr extends MoveEffectAttr {
|
export class TransformAttr extends MoveEffectAttr {
|
||||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
if (!super.apply(user, target, move, args) || (target.isTransformed() || user.isTransformed())) {
|
if (!super.apply(user, target, move, args)) {
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("battle:attackFailed"));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.unshiftNew("PokemonTransformPhase", user.getBattlerIndex(), target.getBattlerIndex());
|
globalScene.phaseManager.unshiftNew("PokemonTransformPhase", user.getBattlerIndex(), target.getBattlerIndex());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCondition(): MoveConditionFunc {
|
||||||
|
return (user, target) => user.canTransformInto(target)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -8840,12 +8844,12 @@ export function initMoves() {
|
|||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new StatusMove(MoveId.TRANSFORM, PokemonType.NORMAL, -1, 10, -1, 0, 1)
|
new StatusMove(MoveId.TRANSFORM, PokemonType.NORMAL, -1, 10, -1, 0, 1)
|
||||||
.attr(TransformAttr)
|
.attr(TransformAttr)
|
||||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
|
|
||||||
.condition((user, target, move) => !target.summonData.illusion && !user.summonData.illusion)
|
|
||||||
// transforming from or into fusion pokemon causes various problems (such as crashes)
|
|
||||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE) && !user.fusionSpecies && !target.fusionSpecies)
|
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
// Transforming should copy the target's rage fist hit count
|
/* Transform:
|
||||||
|
* Does not copy the target's rage fist hit count
|
||||||
|
* Does not copy the target's volatile status conditions (ie BattlerTags)
|
||||||
|
* Renders user typeless when copying typeless opponent (should revert to original typing)
|
||||||
|
*/
|
||||||
.edgeCase(),
|
.edgeCase(),
|
||||||
new AttackMove(MoveId.BUBBLE, PokemonType.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
|
new AttackMove(MoveId.BUBBLE, PokemonType.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
||||||
|
@ -1066,6 +1066,30 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return this.summonData.speciesForm !== null;
|
return this.summonData.speciesForm !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether this Pokemon can transform into an opposing Pokemon.
|
||||||
|
* @param target - The {@linkcode Pokemon} being transformed into.
|
||||||
|
* @returns Whether this Pokemon can transform into `target`.
|
||||||
|
*/
|
||||||
|
canTransformInto(target: Pokemon): boolean {
|
||||||
|
return !(
|
||||||
|
// Neither pokemon can be already transformed
|
||||||
|
(
|
||||||
|
this.isTransformed() ||
|
||||||
|
target.isTransformed() ||
|
||||||
|
// Neither pokemon can be behind an illusion
|
||||||
|
target.summonData.illusion ||
|
||||||
|
this.summonData.illusion ||
|
||||||
|
// The target cannot be behind a substitute
|
||||||
|
target.getTag(BattlerTagType.SUBSTITUTE) ||
|
||||||
|
// Transforming to/from fusion pokemon causes various problems (crashes, etc.)
|
||||||
|
// TODO: Consider lifting restriction once bug is fixed
|
||||||
|
this.isFusion() ||
|
||||||
|
target.isFusion()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} useIllusion - Whether we want the fusionSpeciesForm of the illusion or not.
|
* @param {boolean} useIllusion - Whether we want the fusionSpeciesForm of the illusion or not.
|
||||||
*/
|
*/
|
||||||
@ -1280,39 +1304,39 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the entire set of stats of this {@linkcode Pokemon}.
|
* Retrieves the entire set of stats of this {@linkcode Pokemon}.
|
||||||
* @param bypassSummonData - whether to use actual stats or in-battle overriden stats from Transform; default `true`
|
* @param bypassSummonData - Whether to prefer actual stats (`true`) or in-battle overridden stats (`false`); default `true`
|
||||||
* @returns the numeric values of this {@linkcode Pokemon}'s stats
|
* @returns The numeric values of this {@linkcode Pokemon}'s stats as an array.
|
||||||
*/
|
*/
|
||||||
getStats(bypassSummonData = true): number[] {
|
getStats(bypassSummonData = true): number[] {
|
||||||
if (!bypassSummonData && this.summonData.stats) {
|
if (!bypassSummonData) {
|
||||||
return this.summonData.stats;
|
// Only grab summon data stats if nonzero
|
||||||
|
return this.summonData.stats.map((s, i) => s || this.stats[i]);
|
||||||
}
|
}
|
||||||
return this.stats;
|
return this.stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the corresponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
|
* Retrieves the corresponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
|
||||||
* @param stat the desired {@linkcode PermanentStat}
|
* @param stat - The desired {@linkcode PermanentStat}.
|
||||||
* @param bypassSummonData prefer actual stats (`true` by default) or in-battle overridden stats (`false`)
|
* @param bypassSummonData - Whether to prefer actual stats (`true`) or in-battle overridden stats (`false`); default `true`
|
||||||
* @returns the numeric value of the desired {@linkcode Stat}
|
* @returns The numeric value of the desired {@linkcode Stat}.
|
||||||
*/
|
*/
|
||||||
getStat(stat: PermanentStat, bypassSummonData = true): number {
|
getStat(stat: PermanentStat, bypassSummonData = true): number {
|
||||||
if (!bypassSummonData && this.summonData.stats[stat] !== 0) {
|
if (!bypassSummonData) {
|
||||||
return this.summonData.stats[stat];
|
// 0 = no override
|
||||||
|
return this.summonData.stats[stat] || this.stats[stat];
|
||||||
}
|
}
|
||||||
return this.stats[stat];
|
return this.stats[stat];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the value to the corrseponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
|
* Change one of this {@linkcode Pokemon}'s {@linkcode PermanentStat}s to the specified value.
|
||||||
*
|
* @param stat - The {@linkcode PermanentStat} to be overwritten.
|
||||||
* Note that this does nothing if {@linkcode value} is less than 0.
|
* @param value - The stat value to set. Ignored if `<=0`
|
||||||
* @param stat the desired {@linkcode PermanentStat} to be overwritten
|
* @param bypassSummonData - Whether to write to actual stats (`true`) or in-battle overridden stats (`false`); default `true`
|
||||||
* @param value the desired numeric value
|
|
||||||
* @param bypassSummonData write to actual stats (`true` by default) or in-battle overridden stats (`false`)
|
|
||||||
*/
|
*/
|
||||||
setStat(stat: PermanentStat, value: number, bypassSummonData = true): void {
|
setStat(stat: PermanentStat, value: number, bypassSummonData = true): void {
|
||||||
if (value < 0) {
|
if (value <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1328,31 +1352,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @returns the numeric values of the {@linkcode Pokemon}'s in-battle stat stages if available, a fresh stat stage array otherwise
|
* @returns the numeric values of the {@linkcode Pokemon}'s in-battle stat stages if available, a fresh stat stage array otherwise
|
||||||
*/
|
*/
|
||||||
getStatStages(): number[] {
|
getStatStages(): number[] {
|
||||||
return this.summonData ? this.summonData.statStages : [0, 0, 0, 0, 0, 0, 0];
|
return this.summonData.statStages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the in-battle stage of the specified {@linkcode BattleStat}.
|
* Retrieve the value of the given stat stage for this {@linkcode Pokemon}.
|
||||||
* @param stat the {@linkcode BattleStat} whose stage is desired
|
* @param stat - The {@linkcode BattleStat} to retrieve the stat stage for.
|
||||||
* @returns the stage of the desired {@linkcode BattleStat} if available, 0 otherwise
|
* @returns The value of the desired stat stage as a number within the range `[-6, +6]`.
|
||||||
*/
|
*/
|
||||||
getStatStage(stat: BattleStat): number {
|
getStatStage(stat: BattleStat): number {
|
||||||
return this.summonData ? this.summonData.statStages[stat - 1] : 0;
|
return this.summonData.statStages[stat - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the value to the in-battle stage of the corresponding {@linkcode BattleStat} of the {@linkcode Pokemon}.
|
* Sets this {@linkcode Pokemon}'s in-battle stat stage to the corresponding value.
|
||||||
*
|
* @param stat - The {@linkcode BattleStat} whose stage is to be overwritten.
|
||||||
* Note that, if the value is not within a range of [-6, 6], it will be forced to the closest range bound.
|
* @param value - The value of the stat stage to set, forcibly clamped within the range `[-6, +6]`.
|
||||||
* @param stat the {@linkcode BattleStat} whose stage is to be overwritten
|
|
||||||
* @param value the desired numeric value
|
|
||||||
*/
|
*/
|
||||||
setStatStage(stat: BattleStat, value: number): void {
|
setStatStage(stat: BattleStat, value: number): void {
|
||||||
if (value >= -6) {
|
this.summonData.statStages[stat - 1] = Phaser.Math.Clamp(value, -6, 6);
|
||||||
this.summonData.statStages[stat - 1] = Math.min(value, 6);
|
|
||||||
} else {
|
|
||||||
this.summonData.statStages[stat - 1] = Math.max(value, -6);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3296,7 +3314,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param onField - whether to also check if the pokemon is currently on the field (defaults to true)
|
* @param onField - whether to also check if the pokemon is currently on the field (defaults to true)
|
||||||
*/
|
*/
|
||||||
getOpponents(onField = true): Pokemon[] {
|
getOpponents(onField = true): Pokemon[] {
|
||||||
return ((this.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField()) as Pokemon[]).filter(p =>
|
return (this.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField()).filter(p =>
|
||||||
p.isActive(onField),
|
p.isActive(onField),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,13 @@ export class PokemonTransformPhase extends PokemonPhase {
|
|||||||
super(userIndex);
|
super(userIndex);
|
||||||
|
|
||||||
this.targetIndex = targetIndex;
|
this.targetIndex = targetIndex;
|
||||||
|
|
||||||
this.playSound = playSound;
|
this.playSound = playSound;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override start(): void {
|
public override start(): void {
|
||||||
const user = this.getPokemon();
|
const user = this.getPokemon();
|
||||||
const target = globalScene.getField(true).find(p => p.getBattlerIndex() === this.targetIndex);
|
const target = globalScene.getField()[this.targetIndex];
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
this.end();
|
this.end();
|
||||||
@ -58,6 +59,8 @@ export class PokemonTransformPhase extends PokemonPhase {
|
|||||||
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
|
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
|
||||||
return new PokemonMove(MoveId.NONE);
|
return new PokemonMove(MoveId.NONE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: This should fallback to the target's original typing if none are left (from Burn Up, etc.)
|
||||||
user.summonData.types = target.getTypes();
|
user.summonData.types = target.getTypes();
|
||||||
|
|
||||||
const promises = [user.updateInfo()];
|
const promises = [user.updateInfo()];
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
379
test/moves/transform-imposter.test.ts
Normal file
379
test/moves/transform-imposter.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@ -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],
|
||||||
|
Loading…
Reference in New Issue
Block a user