[Ability] Implement Protean and Libero abilities (#1309)
* Add generic to util holders to reduce manual type casting
* implement protean and libero abilities
* remove use only once per turn trigger
* Revert Attack Attribute Conditions back to requiring unused vars
* Remove conditional before invoking type change ability
* update protean to properly trigger and skip certain moves
* remove some dangerous typecasts
* revert autoformatting changes
* not all autoformatting changes were reverted
* Revert "Add generic to util holders to reduce manual type casting"
This reverts commit 3ee7f1d5ff
.
* change some variable names
* remove incorrect comment
* update abilities so they use gen 9 logic
* fix typescript error from missing Terrain type
* update gameManager switchPokemon to match other menu utilities
* add test cases for protean and libero
This commit is contained in:
parent
048993b2c2
commit
819fe9b4a1
|
@ -9,7 +9,7 @@ import { BattlerTag } from "./battler-tags";
|
||||||
import { BattlerTagType } from "./enums/battler-tag-type";
|
import { BattlerTagType } from "./enums/battler-tag-type";
|
||||||
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||||
import { Gender } from "./gender";
|
import { Gender } from "./gender";
|
||||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr } from "./move";
|
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr } from "./move";
|
||||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||||
import { ArenaTagType } from "./enums/arena-tag-type";
|
import { ArenaTagType } from "./enums/arena-tag-type";
|
||||||
import { Stat, getStatName } from "./pokemon-stat";
|
import { Stat, getStatName } from "./pokemon-stat";
|
||||||
|
@ -1085,21 +1085,20 @@ export class FieldMultiplyBattleStatAbAttr extends AbAttr {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MoveTypeChangeAttr extends PreAttackAbAttr {
|
export class MoveTypeChangeAttr extends PreAttackAbAttr {
|
||||||
private newType: Type;
|
constructor(
|
||||||
private powerMultiplier: number;
|
private newType: Type,
|
||||||
private condition: PokemonAttackCondition;
|
private powerMultiplier: number,
|
||||||
|
private condition?: PokemonAttackCondition
|
||||||
constructor(newType: Type, powerMultiplier: number, condition: PokemonAttackCondition) {
|
) {
|
||||||
super(true);
|
super(true);
|
||||||
this.newType = newType;
|
|
||||||
this.powerMultiplier = powerMultiplier;
|
|
||||||
this.condition = condition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
|
applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
|
||||||
if (this.condition(pokemon, defender, move)) {
|
if (this.condition && this.condition(pokemon, defender, move)) {
|
||||||
move.type = this.newType;
|
move.type = this.newType;
|
||||||
(args[0] as Utils.NumberHolder).value *= this.powerMultiplier;
|
if (args[0] && args[0] instanceof Utils.NumberHolder) {
|
||||||
|
args[0].value *= this.powerMultiplier;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1107,6 +1106,58 @@ export class MoveTypeChangeAttr extends PreAttackAbAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ability attribute for changing a pokemon's type before using a move */
|
||||||
|
export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
||||||
|
private moveType: Type;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
if (
|
||||||
|
!pokemon.isTerastallized() &&
|
||||||
|
move.id !== Moves.STRUGGLE &&
|
||||||
|
/**
|
||||||
|
* Skip moves that call other moves because these moves generate a following move that will trigger this ability attribute
|
||||||
|
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Category:Moves_that_call_other_moves}
|
||||||
|
*/
|
||||||
|
!move.findAttr((attr) =>
|
||||||
|
attr instanceof RandomMovesetMoveAttr ||
|
||||||
|
attr instanceof RandomMoveAttr ||
|
||||||
|
attr instanceof NaturePowerAttr ||
|
||||||
|
attr instanceof CopyMoveAttr
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// TODO remove this copy when phase order is changed so that damage, type, category, etc.
|
||||||
|
// TODO are all calculated prior to playing the move animation.
|
||||||
|
const moveCopy = new Move(move.id, move.type, move.category, move.moveTarget, move.power, move.accuracy, move.pp, move.chance, move.priority, move.generation);
|
||||||
|
moveCopy.attrs = move.attrs;
|
||||||
|
|
||||||
|
// Moves like Weather Ball ignore effects of abilities like Normalize and Refrigerate
|
||||||
|
if (move.findAttr(attr => attr instanceof VariableMoveTypeAttr)) {
|
||||||
|
applyMoveAttrs(VariableMoveTypeAttr, pokemon, null, moveCopy);
|
||||||
|
} else {
|
||||||
|
applyPreAttackAbAttrs(MoveTypeChangeAttr, pokemon, null, moveCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pokemon.getTypes().some((t) => t !== moveCopy.type)) {
|
||||||
|
this.moveType = moveCopy.type;
|
||||||
|
pokemon.summonData.types = [moveCopy.type];
|
||||||
|
pokemon.updateInfo();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
|
||||||
|
return getPokemonMessage(pokemon, ` transformed into the ${Type[this.moveType]} type!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for abilities that boost the damage of moves
|
* Class for abilities that boost the damage of moves
|
||||||
* For abilities that boost the base power of moves, see VariableMovePowerAbAttr
|
* For abilities that boost the base power of moves, see VariableMovePowerAbAttr
|
||||||
|
@ -3557,6 +3608,9 @@ function applyAbAttrsInternal<TAttr extends AbAttr>(attrType: { new(...args: any
|
||||||
}
|
}
|
||||||
pokemon.scene.setPhaseQueueSplice();
|
pokemon.scene.setPhaseQueueSplice();
|
||||||
const onApplySuccess = () => {
|
const onApplySuccess = () => {
|
||||||
|
if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) {
|
||||||
|
pokemon.summonData.abilitiesApplied.push(ability.id);
|
||||||
|
}
|
||||||
if (pokemon.battleData && !pokemon.battleData.abilitiesApplied.includes(ability.id)) {
|
if (pokemon.battleData && !pokemon.battleData.abilitiesApplied.includes(ability.id)) {
|
||||||
pokemon.battleData.abilitiesApplied.push(ability.id);
|
pokemon.battleData.abilitiesApplied.push(ability.id);
|
||||||
}
|
}
|
||||||
|
@ -4039,8 +4093,9 @@ export function initAbilities() {
|
||||||
.conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
|
.conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, BattleStatMultiplierAbAttr, BattleStat.SPD, 2)
|
||||||
.conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.SPD, 1.5),
|
.conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.SPD, 1.5),
|
||||||
new Ability(Abilities.NORMALIZE, 4)
|
new Ability(Abilities.NORMALIZE, 4)
|
||||||
.attr(MoveTypeChangeAttr, Type.NORMAL, 1.2, (user, target, move) => move.id !== Moves.HIDDEN_POWER && move.id !== Moves.WEATHER_BALL &&
|
.attr(MoveTypeChangeAttr, Type.NORMAL, 1.2, (user, target, move) => {
|
||||||
move.id !== Moves.NATURAL_GIFT && move.id !== Moves.JUDGMENT && move.id !== Moves.TECHNO_BLAST),
|
return ![Moves.HIDDEN_POWER, Moves.WEATHER_BALL, Moves.NATURAL_GIFT, Moves.JUDGMENT, Moves.TECHNO_BLAST].includes(move.id);
|
||||||
|
}),
|
||||||
new Ability(Abilities.SNIPER, 4)
|
new Ability(Abilities.SNIPER, 4)
|
||||||
.attr(MultCritAbAttr, 1.5),
|
.attr(MultCritAbAttr, 1.5),
|
||||||
new Ability(Abilities.MAGIC_GUARD, 4)
|
new Ability(Abilities.MAGIC_GUARD, 4)
|
||||||
|
@ -4259,7 +4314,8 @@ export function initAbilities() {
|
||||||
.attr(HealFromBerryUseAbAttr, 1/3)
|
.attr(HealFromBerryUseAbAttr, 1/3)
|
||||||
.partial(), // Healing not blocked by Heal Block
|
.partial(), // Healing not blocked by Heal Block
|
||||||
new Ability(Abilities.PROTEAN, 6)
|
new Ability(Abilities.PROTEAN, 6)
|
||||||
.unimplemented(),
|
.attr(PokemonTypeChangeAbAttr)
|
||||||
|
.condition((p) => !p.summonData?.abilitiesApplied.includes(Abilities.PROTEAN)),
|
||||||
new Ability(Abilities.FUR_COAT, 6)
|
new Ability(Abilities.FUR_COAT, 6)
|
||||||
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, 0.5)
|
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, 0.5)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
|
@ -4498,7 +4554,8 @@ export function initAbilities() {
|
||||||
.attr(PostSummonStatChangeAbAttr, BattleStat.DEF, 1, true)
|
.attr(PostSummonStatChangeAbAttr, BattleStat.DEF, 1, true)
|
||||||
.condition(getOncePerBattleCondition(Abilities.DAUNTLESS_SHIELD)),
|
.condition(getOncePerBattleCondition(Abilities.DAUNTLESS_SHIELD)),
|
||||||
new Ability(Abilities.LIBERO, 8)
|
new Ability(Abilities.LIBERO, 8)
|
||||||
.unimplemented(),
|
.attr(PokemonTypeChangeAbAttr)
|
||||||
|
.condition((p) => !p.summonData?.abilitiesApplied.includes(Abilities.LIBERO)),
|
||||||
new Ability(Abilities.BALL_FETCH, 8)
|
new Ability(Abilities.BALL_FETCH, 8)
|
||||||
.attr(FetchBallAbAttr)
|
.attr(FetchBallAbAttr)
|
||||||
.condition(getOncePerBattleCondition(Abilities.BALL_FETCH)),
|
.condition(getOncePerBattleCondition(Abilities.BALL_FETCH)),
|
||||||
|
|
|
@ -3811,6 +3811,7 @@ export class PokemonSummonData {
|
||||||
public disabledTurns: integer = 0;
|
public disabledTurns: integer = 0;
|
||||||
public tags: BattlerTag[] = [];
|
public tags: BattlerTag[] = [];
|
||||||
public abilitySuppressed: boolean = false;
|
public abilitySuppressed: boolean = false;
|
||||||
|
public abilitiesApplied: Abilities[] = [];
|
||||||
|
|
||||||
public speciesForm: PokemonSpeciesForm;
|
public speciesForm: PokemonSpeciesForm;
|
||||||
public fusionSpeciesForm: PokemonSpeciesForm;
|
public fusionSpeciesForm: PokemonSpeciesForm;
|
||||||
|
@ -3819,7 +3820,8 @@ export class PokemonSummonData {
|
||||||
public fusionGender: Gender;
|
public fusionGender: Gender;
|
||||||
public stats: integer[];
|
public stats: integer[];
|
||||||
public moveset: PokemonMove[];
|
public moveset: PokemonMove[];
|
||||||
public types: Type[];
|
// If not initialized this value will not be populated from save data.
|
||||||
|
public types: Type[] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PokemonBattleData {
|
export class PokemonBattleData {
|
||||||
|
@ -3831,7 +3833,9 @@ export class PokemonBattleData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PokemonBattleSummonData {
|
export class PokemonBattleSummonData {
|
||||||
|
/** The number of turns the pokemon has passed since entering the battle */
|
||||||
public turnCount: integer = 1;
|
public turnCount: integer = 1;
|
||||||
|
/** The list of moves the pokemon has used since entering the battle */
|
||||||
public moveHistory: TurnMove[] = [];
|
public moveHistory: TurnMove[] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, get
|
||||||
import { TempBattleStat } from "./data/temp-battle-stat";
|
import { TempBattleStat } from "./data/temp-battle-stat";
|
||||||
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
|
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
|
||||||
import { ArenaTagType } from "./data/enums/arena-tag-type";
|
import { ArenaTagType } from "./data/enums/arena-tag-type";
|
||||||
import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr } from "./data/ability";
|
import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr } from "./data/ability";
|
||||||
import { Unlockables, getUnlockableName } from "./system/unlockables";
|
import { Unlockables, getUnlockableName } from "./system/unlockables";
|
||||||
import { getBiomeKey } from "./field/arena";
|
import { getBiomeKey } from "./field/arena";
|
||||||
import { BattleType, BattlerIndex, TurnCommand } from "./battle";
|
import { BattleType, BattlerIndex, TurnCommand } from "./battle";
|
||||||
|
@ -2692,6 +2692,16 @@ export class MovePhase extends BattlePhase {
|
||||||
failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain.terrainType);
|
failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain.terrainType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger pokemon type change before playing the move animation
|
||||||
|
* Will still change the user's type when using Roar, Whirlwind, Trick-or-Treat, and Forest's Curse,
|
||||||
|
* regardless of whether the move successfully executes or not.
|
||||||
|
*/
|
||||||
|
if (success || [Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) {
|
||||||
|
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.scene.unshiftPhase(this.getEffectPhase());
|
this.scene.unshiftPhase(this.getEffectPhase());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,16 +2,19 @@ import { Arena } from "../field/arena";
|
||||||
import { ArenaTag } from "../data/arena-tag";
|
import { ArenaTag } from "../data/arena-tag";
|
||||||
import { Biome } from "../data/enums/biome";
|
import { Biome } from "../data/enums/biome";
|
||||||
import { Weather } from "../data/weather";
|
import { Weather } from "../data/weather";
|
||||||
|
import { Terrain } from "#app/data/terrain.js";
|
||||||
|
|
||||||
export default class ArenaData {
|
export default class ArenaData {
|
||||||
public biome: Biome;
|
public biome: Biome;
|
||||||
public weather: Weather;
|
public weather: Weather;
|
||||||
|
public terrain: Terrain;
|
||||||
public tags: ArenaTag[];
|
public tags: ArenaTag[];
|
||||||
|
|
||||||
constructor(source: Arena | any) {
|
constructor(source: Arena | any) {
|
||||||
const sourceArena = source instanceof Arena ? source as Arena : null;
|
const sourceArena = source instanceof Arena ? source as Arena : null;
|
||||||
this.biome = sourceArena ? sourceArena.biomeType : source.biome;
|
this.biome = sourceArena ? sourceArena.biomeType : source.biome;
|
||||||
this.weather = sourceArena ? sourceArena.weather : source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : undefined;
|
this.weather = sourceArena ? sourceArena.weather : source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : undefined;
|
||||||
|
this.terrain = sourceArena ? sourceArena.terrain : source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : undefined;
|
||||||
this.tags = sourceArena ? sourceArena.tags : [];
|
this.tags = sourceArena ? sourceArena.tags : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -829,7 +829,7 @@ export class GameData {
|
||||||
loadSession(scene: BattleScene, slotId: integer, sessionData?: SessionSaveData): Promise<boolean> {
|
loadSession(scene: BattleScene, slotId: integer, sessionData?: SessionSaveData): Promise<boolean> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const initSessionFromData = async sessionData => {
|
const initSessionFromData = async (sessionData: SessionSaveData) => {
|
||||||
console.debug(sessionData);
|
console.debug(sessionData);
|
||||||
|
|
||||||
scene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC);
|
scene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC);
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default class PokemonData {
|
||||||
public summonData: PokemonSummonData;
|
public summonData: PokemonSummonData;
|
||||||
|
|
||||||
constructor(source: Pokemon | any, forHistory: boolean = false) {
|
constructor(source: Pokemon | any, forHistory: boolean = false) {
|
||||||
const sourcePokemon = source instanceof Pokemon ? source as Pokemon : null;
|
const sourcePokemon = source instanceof Pokemon ? source : null;
|
||||||
this.id = source.id;
|
this.id = source.id;
|
||||||
this.player = sourcePokemon ? sourcePokemon.isPlayer() : source.player;
|
this.player = sourcePokemon ? sourcePokemon.isPlayer() : source.player;
|
||||||
this.species = sourcePokemon ? sourcePokemon.species.speciesId : source.species;
|
this.species = sourcePokemon ? sourcePokemon.species.speciesId : source.species;
|
||||||
|
@ -121,6 +121,7 @@ export default class PokemonData {
|
||||||
this.summonData.disabledMove = source.summonData.disabledMove;
|
this.summonData.disabledMove = source.summonData.disabledMove;
|
||||||
this.summonData.disabledTurns = source.summonData.disabledTurns;
|
this.summonData.disabledTurns = source.summonData.disabledTurns;
|
||||||
this.summonData.abilitySuppressed = source.summonData.abilitySuppressed;
|
this.summonData.abilitySuppressed = source.summonData.abilitySuppressed;
|
||||||
|
this.summonData.abilitiesApplied = source.summonData.abilitiesApplied;
|
||||||
|
|
||||||
this.summonData.ability = source.summonData.ability;
|
this.summonData.ability = source.summonData.ability;
|
||||||
this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m));
|
this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m));
|
||||||
|
|
|
@ -0,0 +1,364 @@
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import GameManager from "../utils/gameManager";
|
||||||
|
import * as Overrides from "#app/overrides";
|
||||||
|
import { Species } from "#app/data/enums/species.js";
|
||||||
|
import { Abilities } from "#app/data/enums/abilities.js";
|
||||||
|
import { Moves } from "#app/data/enums/moves.js";
|
||||||
|
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||||
|
import { MoveEffectPhase, TurnEndPhase } from "#app/phases.js";
|
||||||
|
import { allMoves } from "#app/data/move.js";
|
||||||
|
import { BattlerTagType } from "#app/data/enums/battler-tag-type.js";
|
||||||
|
import { Weather, WeatherType } from "#app/data/weather.js";
|
||||||
|
import { Type } from "#app/data/type.js";
|
||||||
|
import { Biome } from "#app/data/enums/biome.js";
|
||||||
|
import { PlayerPokemon } from "#app/field/pokemon.js";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Abilities - Protean", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.LIBERO);
|
||||||
|
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ENDURE, Moves.ENDURE, Moves.ENDURE, Moves.ENDURE]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies and changes a pokemon's type",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies only once per switch in",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.AGILITY]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP, Species.BULBASAUR]);
|
||||||
|
|
||||||
|
let leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.AGILITY));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied.filter((a) => a === Abilities.LIBERO)).toHaveLength(1);
|
||||||
|
const leadPokemonType = Type[leadPokemon.getTypes()[0]];
|
||||||
|
const moveType = Type[allMoves[Moves.AGILITY].defaultType];
|
||||||
|
expect(leadPokemonType).not.toBe(moveType);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move has a variable type",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.WEATHER_BALL]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.scene.arena.weather = new Weather(WeatherType.SUNNY);
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.WEATHER_BALL));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO);
|
||||||
|
expect(leadPokemon.getTypes()).toHaveLength(1);
|
||||||
|
const leadPokemonType = Type[leadPokemon.getTypes()[0]],
|
||||||
|
moveType = Type[Type.FIRE];
|
||||||
|
expect(leadPokemonType).toBe(moveType);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the type has changed by another ability",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.REFRIGERATE);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO);
|
||||||
|
expect(leadPokemon.getTypes()).toHaveLength(1);
|
||||||
|
const leadPokemonType = Type[leadPokemon.getTypes()[0]],
|
||||||
|
moveType = Type[Type.ICE];
|
||||||
|
expect(leadPokemonType).toBe(moveType);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move calls another move",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.NATURE_POWER]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.scene.arena.biomeType = Biome.MOUNTAIN;
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.NATURE_POWER));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.AIR_SLASH);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move is delayed / charging",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DIG]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DIG));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.DIG);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move misses",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValueOnce(false);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move is protected against",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.PROTECT, Moves.PROTECT, Moves.PROTECT, Moves.PROTECT]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move fails because of type immunity",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.GASTLY);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability is not applied if pokemon's type is the same as the move's type",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].defaultType];
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability is not applied if pokemon is terastallized",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
vi.spyOn(leadPokemon, "isTerastallized").mockReturnValue(true);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability is not applied if pokemon uses struggle",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.STRUGGLE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.STRUGGLE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability is not applied if the pokemon's move fails",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.BURN_UP]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.BURN_UP));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.LIBERO);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's Trick-or-Treat fails",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TRICK_OR_TREAT]);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.GASTLY);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TRICK_OR_TREAT));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TRICK_OR_TREAT);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly and the pokemon curses itself",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.CURSE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.CURSE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.CURSE);
|
||||||
|
expect(leadPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: Moves) {
|
||||||
|
expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.LIBERO);
|
||||||
|
expect(pokemon.getTypes()).toHaveLength(1);
|
||||||
|
const pokemonType = Type[pokemon.getTypes()[0]],
|
||||||
|
moveType = Type[allMoves[move].defaultType];
|
||||||
|
expect(pokemonType).toBe(moveType);
|
||||||
|
}
|
|
@ -0,0 +1,364 @@
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import GameManager from "../utils/gameManager";
|
||||||
|
import * as Overrides from "#app/overrides";
|
||||||
|
import { Species } from "#app/data/enums/species.js";
|
||||||
|
import { Abilities } from "#app/data/enums/abilities.js";
|
||||||
|
import { Moves } from "#app/data/enums/moves.js";
|
||||||
|
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||||
|
import { MoveEffectPhase, TurnEndPhase } from "#app/phases.js";
|
||||||
|
import { allMoves } from "#app/data/move.js";
|
||||||
|
import { BattlerTagType } from "#app/data/enums/battler-tag-type.js";
|
||||||
|
import { Weather, WeatherType } from "#app/data/weather.js";
|
||||||
|
import { Type } from "#app/data/type.js";
|
||||||
|
import { Biome } from "#app/data/enums/biome.js";
|
||||||
|
import { PlayerPokemon } from "#app/field/pokemon.js";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Abilities - Protean", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.PROTEAN);
|
||||||
|
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ENDURE, Moves.ENDURE, Moves.ENDURE, Moves.ENDURE]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies and changes a pokemon's type",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies only once per switch in",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.AGILITY]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP, Species.BULBASAUR]);
|
||||||
|
|
||||||
|
let leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.AGILITY));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied.filter((a) => a === Abilities.PROTEAN)).toHaveLength(1);
|
||||||
|
const leadPokemonType = Type[leadPokemon.getTypes()[0]];
|
||||||
|
const moveType = Type[allMoves[Moves.AGILITY].defaultType];
|
||||||
|
expect(leadPokemonType).not.toBe(moveType);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.SPLASH);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move has a variable type",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.WEATHER_BALL]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.scene.arena.weather = new Weather(WeatherType.SUNNY);
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.WEATHER_BALL));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN);
|
||||||
|
expect(leadPokemon.getTypes()).toHaveLength(1);
|
||||||
|
const leadPokemonType = Type[leadPokemon.getTypes()[0]],
|
||||||
|
moveType = Type[Type.FIRE];
|
||||||
|
expect(leadPokemonType).toBe(moveType);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the type has changed by another ability",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.REFRIGERATE);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN);
|
||||||
|
expect(leadPokemon.getTypes()).toHaveLength(1);
|
||||||
|
const leadPokemonType = Type[leadPokemon.getTypes()[0]],
|
||||||
|
moveType = Type[Type.ICE];
|
||||||
|
expect(leadPokemonType).toBe(moveType);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move calls another move",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.NATURE_POWER]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.scene.arena.biomeType = Biome.MOUNTAIN;
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.NATURE_POWER));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.AIR_SLASH);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move is delayed / charging",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DIG]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DIG));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.DIG);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move misses",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValueOnce(false);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move is protected against",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.PROTECT, Moves.PROTECT, Moves.PROTECT, Moves.PROTECT]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's move fails because of type immunity",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.GASTLY);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability is not applied if pokemon's type is the same as the move's type",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
leadPokemon.summonData.types = [allMoves[Moves.SPLASH].defaultType];
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability is not applied if pokemon is terastallized",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
vi.spyOn(leadPokemon, "isTerastallized").mockReturnValue(true);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability is not applied if pokemon uses struggle",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.STRUGGLE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.STRUGGLE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability is not applied if the pokemon's move fails",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.BURN_UP]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.BURN_UP));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.abilitiesApplied).not.toContain(Abilities.PROTEAN);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly even if the pokemon's Trick-or-Treat fails",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TRICK_OR_TREAT]);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.GASTLY);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TRICK_OR_TREAT));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TRICK_OR_TREAT);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability applies correctly and the pokemon curses itself",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.CURSE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.CURSE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.CURSE);
|
||||||
|
expect(leadPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined);
|
||||||
|
},
|
||||||
|
TIMEOUT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: Moves) {
|
||||||
|
expect(pokemon.summonData.abilitiesApplied).toContain(Abilities.PROTEAN);
|
||||||
|
expect(pokemon.getTypes()).toHaveLength(1);
|
||||||
|
const pokemonType = Type[pokemon.getTypes()[0]],
|
||||||
|
moveType = Type[allMoves[move].defaultType];
|
||||||
|
expect(pokemonType).toBe(moveType);
|
||||||
|
}
|
Loading…
Reference in New Issue