diff --git a/src/battle.ts b/src/battle.ts index 75f0dff2534..1c6ba788e19 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -7,7 +7,7 @@ import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/mod import { PokeballType } from "#enums/pokeball"; import { trainerConfigs } from "#app/data/trainer-config"; import { SpeciesFormKey } from "#enums/species-form-key"; -import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon"; +import Pokemon, { EnemyPokemon, PlayerPokemon, TurnMove } from "#app/field/pokemon"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleSpec } from "#enums/battle-spec"; import { Moves } from "#enums/moves"; @@ -44,12 +44,12 @@ export enum BattlerIndex { } export interface TurnCommand { - command: Command; - cursor?: number; - move?: QueuedMove; - targets?: BattlerIndex[]; - skip?: boolean; - args?: any[]; + command: Command; + cursor?: number; + move?: TurnMove; + targets?: BattlerIndex[]; + skip?: boolean; + args?: any[]; } export interface FaintLogEntry { @@ -91,6 +91,7 @@ export default class Battle { public playerFaintsHistory: FaintLogEntry[] = []; public enemyFaintsHistory: FaintLogEntry[] = []; + public mysteryEncounterType?: MysteryEncounterType; /** If the current battle is a Mystery Encounter, this will always be defined */ public mysteryEncounter?: MysteryEncounter; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 75b7cddb904..6d398471a3c 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -610,7 +610,7 @@ export class InterruptedTag extends BattlerTag { super.onAdd(pokemon); pokemon.getMoveQueue().shift(); - pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.OTHER }); + pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.OTHER, targets: []}); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { diff --git a/src/data/move.ts b/src/data/move.ts index 166058178f5..d388ec89255 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -81,7 +81,6 @@ export enum MoveFlags { NONE = 0, MAKES_CONTACT = 1 << 0, IGNORE_PROTECT = 1 << 1, - IGNORE_VIRTUAL = 1 << 2, /** * Sound-based moves have the following effects: * - Pokemon with the {@linkcode Abilities.SOUNDPROOF Soundproof Ability} are unaffected by other Pokemon's sound-based moves. @@ -92,35 +91,35 @@ export enum MoveFlags { * * cf https://bulbapedia.bulbagarden.net/wiki/Sound-based_move */ - SOUND_BASED = 1 << 3, - HIDE_USER = 1 << 4, - HIDE_TARGET = 1 << 5, - BITING_MOVE = 1 << 6, - PULSE_MOVE = 1 << 7, - PUNCHING_MOVE = 1 << 8, - SLICING_MOVE = 1 << 9, + SOUND_BASED = 1 << 2, + HIDE_USER = 1 << 3, + HIDE_TARGET = 1 << 4, + BITING_MOVE = 1 << 5, + PULSE_MOVE = 1 << 6, + PUNCHING_MOVE = 1 << 7, + SLICING_MOVE = 1 << 8, /** * Indicates a move should be affected by {@linkcode Abilities.RECKLESS} * @see {@linkcode Move.recklessMove()} */ - RECKLESS_MOVE = 1 << 10, + RECKLESS_MOVE = 1 << 9, /** Indicates a move should be affected by {@linkcode Abilities.BULLETPROOF} */ - BALLBOMB_MOVE = 1 << 11, + BALLBOMB_MOVE = 1 << 10, /** Grass types and pokemon with {@linkcode Abilities.OVERCOAT} are immune to powder moves */ - POWDER_MOVE = 1 << 12, + POWDER_MOVE = 1 << 11, /** Indicates a move should trigger {@linkcode Abilities.DANCER} */ - DANCE_MOVE = 1 << 13, + DANCE_MOVE = 1 << 12, /** Indicates a move should trigger {@linkcode Abilities.WIND_RIDER} */ - WIND_MOVE = 1 << 14, + WIND_MOVE = 1 << 13, /** Indicates a move should trigger {@linkcode Abilities.TRIAGE} */ - TRIAGE_MOVE = 1 << 15, - IGNORE_ABILITIES = 1 << 16, + TRIAGE_MOVE = 1 << 14, + IGNORE_ABILITIES = 1 << 15, /** Enables all hits of a multi-hit move to be accuracy checked individually */ - CHECK_ALL_HITS = 1 << 17, + CHECK_ALL_HITS = 1 << 16, /** Indicates a move is able to bypass its target's Substitute (if the target has one) */ - IGNORE_SUBSTITUTE = 1 << 18, + IGNORE_SUBSTITUTE = 1 << 17, /** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */ - REDIRECT_COUNTER = 1 << 19, + REDIRECT_COUNTER = 1 << 18, } type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; @@ -435,16 +434,6 @@ export default class Move implements Localizable { return this; } - /** - * Sets the {@linkcode MoveFlags.IGNORE_VIRTUAL} flag for the calling Move - * @see {@linkcode Moves.NATURE_POWER} - * @returns The {@linkcode Move} that called this function - */ - ignoresVirtual(): this { - this.setFlag(MoveFlags.IGNORE_VIRTUAL, true); - return this; - } - /** * Sets the {@linkcode MoveFlags.SOUND_BASED} flag for the calling Move * @see {@linkcode Moves.UPROAR} @@ -1546,7 +1535,7 @@ export class RecoilAttr extends MoveEffectAttr { } // Chloroblast and Struggle should not deal recoil damage if the move was not successful - if (this.useHp && [ MoveResult.FAIL, MoveResult.MISS ].includes(user.getLastXMoves(1)[0]?.result)) { + if (this.useHp && [ MoveResult.FAIL, MoveResult.MISS ].includes(user.getLastXMoves(1)[0]?.result ?? MoveResult.FAIL)) { return false; } @@ -6465,52 +6454,40 @@ export class FirstMoveTypeAttr extends MoveEffectAttr { } } -export class RandomMovesetMoveAttr extends OverrideMoveEffectAttr { - private enemyMoveset: boolean | null; - - constructor(enemyMoveset?: boolean) { - super(); - - this.enemyMoveset = enemyMoveset!; // TODO: is this bang correct? - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const moveset = (!this.enemyMoveset ? user : target).getMoveset(); - const moves = moveset.filter(m => !m?.getMove().hasFlag(MoveFlags.IGNORE_VIRTUAL)); - if (moves.length) { - const move = moves[user.randSeedInt(moves.length)]; - const moveIndex = moveset.findIndex(m => m?.moveId === move?.moveId); - const moveTargets = getMoveTargets(user, move?.moveId!); // TODO: is this bang correct? - if (!moveTargets.targets.length) { - return false; - } - let selectTargets: BattlerIndex[]; - switch (true) { - case (moveTargets.multiple || moveTargets.targets.length === 1): { - selectTargets = moveTargets.targets; - break; - } - case (moveTargets.targets.indexOf(target.getBattlerIndex()) > -1): { - selectTargets = [ target.getBattlerIndex() ]; - break; - } - default: { - moveTargets.targets.splice(moveTargets.targets.indexOf(user.getAlly().getBattlerIndex())); - selectTargets = [ moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; - break; - } - } - const targets = selectTargets; - user.getMoveQueue().push({ move: move?.moveId!, targets: targets, ignorePP: true }); // TODO: is this bang correct? - user.scene.unshiftPhase(new MovePhase(user.scene, user, targets, moveset[moveIndex]!, true)); // There's a PR to re-do the move(s) that use this Attr, gonna put `!` for now - return true; +export class CallMoveAttr extends OverrideMoveEffectAttr { + protected invalidMoves: Moves[]; + protected hasTarget: boolean; + async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined; + const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget); + if (moveTargets.targets.length === 0) { + return false; } + const targets = moveTargets.multiple || moveTargets.targets.length === 1 + ? moveTargets.targets + : [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already + user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true }); + user.scene.unshiftPhase(new MovePhase(user.scene, user, targets, new PokemonMove(move.id, 0, 0, true), true, true)); - return false; + await Promise.resolve(initMoveAnim(user.scene, move.id).then(() => { + loadMoveAnimAssets(user.scene, [ move.id ], true); + })); + return true; } } -export class RandomMoveAttr extends OverrideMoveEffectAttr { +/** + * Attribute used to call a random move + * Used for {@linkcode Moves.METRONOME} + * @see {@linkcode apply} for move selection and move call + * @extends OverrideMoveEffectAttr + */ +export class RandomMoveAttr extends CallMoveAttr { + constructor(invalidMoves: Moves[]) { + super(); + this.invalidMoves = invalidMoves; + } + /** * This function exists solely to allow tests to override the randomly selected move by mocking this function. */ @@ -6518,31 +6495,352 @@ export class RandomMoveAttr extends OverrideMoveEffectAttr { return null; } + /** + * User calls a random moveId + * @param {Pokemon} user Pokemon that used the move and will call a random move + * @param {Pokemon} target Pokemon that will be targeted by the random move (if single target) + * @param {Move} move Move being used + * @param {any[]} args Unused + * @returns {Promise} + * Invalid moves are indicated by what is passed in to invalidMoves: @constant {invalidMetronomeMoves} + */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - const moveIds = Utils.getEnumValues(Moves).filter(m => !allMoves[m].hasFlag(MoveFlags.IGNORE_VIRTUAL) && !allMoves[m].name.endsWith(" (N)")); - const moveId = this.getMoveOverride() ?? moveIds[user.randSeedInt(moveIds.length)]; + const moveIds = Utils.getEnumValues(Moves).map(m => !this.invalidMoves.includes(m) && !allMoves[m].name.endsWith(" (N)") ? m : Moves.NONE); + let moveId: Moves = Moves.NONE; + do { + moveId = this.getMoveOverride() ?? moveIds[user.randSeedInt(moveIds.length)]; + } + while (moveId === Moves.NONE); + return super.apply(user, target, allMoves[moveId], args); + } +} - const moveTargets = getMoveTargets(user, moveId); - if (!moveTargets.targets.length) { - resolve(false); - return; +/** + * Attribute used to call a random move in the user or party's moveset + * Used for {@linkcode Moves.ASSIST} and {@linkcode Moves.SLEEP_TALK} + * @extends RandomMoveAttr to use the callMove function on a moveId + * @see {@linkcode getCondition} for move selection + * Fails if the user has no callable moves + * Invalid moves are indicated by what is passed in to invalidMoves: {@constant invalidAssistMoves} or {@constant invalidSleepTalkMoves} + */ +export class RandomMovesetMoveAttr extends CallMoveAttr { + private includeParty: boolean; + private moveId: number; + constructor(invalidMoves: Moves[], includeParty: boolean = false) { + super(); + this.includeParty = includeParty; + this.invalidMoves = invalidMoves; + } + + /** + * User calls a random moveId selected in {@linkcode getCondition} + * @param {Pokemon} user Pokemon that used the move and will call a random move + * @param {Pokemon} target Pokemon that will be targeted by the random move (if single target) + * @param {Move} move Move being used + * @param {any[]} args Unused + * @returns {Promise} + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + return super.apply(user, target, allMoves[this.moveId], args); + } + + getCondition(): MoveConditionFunc { + return (user, target, move) => { + // includeParty will be true for Assist, false for Sleep Talk + let allies: Pokemon[]; + if (this.includeParty) { + allies = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); + } else { + allies = [ user ]; } - const targets = moveTargets.multiple || moveTargets.targets.length === 1 - ? moveTargets.targets - : moveTargets.targets.indexOf(target.getBattlerIndex()) > -1 - ? [ target.getBattlerIndex() ] - : [ moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; - user.getMoveQueue().push({ move: moveId, targets: targets, ignorePP: true }); - user.scene.unshiftPhase(new MovePhase(user.scene, user, targets, new PokemonMove(moveId, 0, 0, true), true)); - initMoveAnim(user.scene, moveId).then(() => { - loadMoveAnimAssets(user.scene, [ moveId ], true) - .then(() => resolve(true)); - }); - }); + const partyMoveset = allies.map(p => p.moveset).flat(); + const moves = partyMoveset.filter(m => !this.invalidMoves.includes(m!.moveId) && !m!.getMove().name.endsWith(" (N)")); + if (moves.length === 0) { + return false; + } + + this.moveId = moves[user.randSeedInt(moves.length)]!.moveId; + return true; + }; } } +const invalidMetronomeMoves: Moves[] = [ + Moves.AFTER_YOU, + Moves.APPLE_ACID, + Moves.ARMOR_CANNON, + Moves.ASSIST, + Moves.ASTRAL_BARRAGE, + Moves.AURA_WHEEL, + Moves.BANEFUL_BUNKER, + Moves.BEAK_BLAST, + Moves.BEHEMOTH_BASH, + Moves.BEHEMOTH_BLADE, + Moves.BELCH, + Moves.BESTOW, + Moves.BLAZING_TORQUE, + Moves.BODY_PRESS, + Moves.BRANCH_POKE, + Moves.BREAKING_SWIPE, + Moves.CELEBRATE, + Moves.CHATTER, + Moves.CHILLING_WATER, + Moves.CHILLY_RECEPTION, + Moves.CLANGOROUS_SOUL, + Moves.COLLISION_COURSE, + Moves.COMBAT_TORQUE, + Moves.COMEUPPANCE, + Moves.COPYCAT, + Moves.COUNTER, + Moves.COVET, + Moves.CRAFTY_SHIELD, + Moves.DECORATE, + Moves.DESTINY_BOND, + Moves.DETECT, + Moves.DIAMOND_STORM, + Moves.DOODLE, + Moves.DOUBLE_IRON_BASH, + Moves.DOUBLE_SHOCK, + Moves.DRAGON_ASCENT, + Moves.DRAGON_ENERGY, + Moves.DRUM_BEATING, + Moves.DYNAMAX_CANNON, + Moves.ELECTRO_DRIFT, + Moves.ENDURE, + Moves.ETERNABEAM, + Moves.FALSE_SURRENDER, + Moves.FEINT, + Moves.FIERY_WRATH, + Moves.FILLET_AWAY, + Moves.FLEUR_CANNON, + Moves.FOCUS_PUNCH, + Moves.FOLLOW_ME, + Moves.FREEZE_SHOCK, + Moves.FREEZING_GLARE, + Moves.GLACIAL_LANCE, + Moves.GRAV_APPLE, + Moves.HELPING_HAND, + Moves.HOLD_HANDS, + Moves.HYPER_DRILL, + Moves.HYPERSPACE_FURY, + Moves.HYPERSPACE_HOLE, + Moves.ICE_BURN, + Moves.INSTRUCT, + Moves.JET_PUNCH, + Moves.JUNGLE_HEALING, + Moves.KINGS_SHIELD, + Moves.LIFE_DEW, + Moves.LIGHT_OF_RUIN, + Moves.MAKE_IT_RAIN, + Moves.MAGICAL_TORQUE, + Moves.MAT_BLOCK, + Moves.ME_FIRST, + Moves.METEOR_ASSAULT, + Moves.METRONOME, + Moves.MIMIC, + Moves.MIND_BLOWN, + Moves.MIRROR_COAT, + Moves.MIRROR_MOVE, + Moves.MOONGEIST_BEAM, + Moves.NATURE_POWER, + Moves.NATURES_MADNESS, + Moves.NOXIOUS_TORQUE, + Moves.OBSTRUCT, + Moves.ORDER_UP, + Moves.ORIGIN_PULSE, + Moves.OVERDRIVE, + Moves.PHOTON_GEYSER, + Moves.PLASMA_FISTS, + Moves.POPULATION_BOMB, + Moves.POUNCE, + Moves.POWER_SHIFT, + Moves.PRECIPICE_BLADES, + Moves.PROTECT, + Moves.PYRO_BALL, + Moves.QUASH, + Moves.QUICK_GUARD, + Moves.RAGE_FIST, + Moves.RAGE_POWDER, + Moves.RAGING_BULL, + Moves.RAGING_FURY, + Moves.RELIC_SONG, + Moves.REVIVAL_BLESSING, + Moves.RUINATION, + Moves.SALT_CURE, + Moves.SECRET_SWORD, + Moves.SHED_TAIL, + Moves.SHELL_TRAP, + Moves.SILK_TRAP, + Moves.SKETCH, + Moves.SLEEP_TALK, + Moves.SNAP_TRAP, + Moves.SNARL, + Moves.SNATCH, + Moves.SNORE, + Moves.SNOWSCAPE, + Moves.SPECTRAL_THIEF, + Moves.SPICY_EXTRACT, + Moves.SPIKY_SHIELD, + Moves.SPIRIT_BREAK, + Moves.SPOTLIGHT, + Moves.STEAM_ERUPTION, + Moves.STEEL_BEAM, + Moves.STRANGE_STEAM, + Moves.STRUGGLE, + Moves.SUNSTEEL_STRIKE, + Moves.SURGING_STRIKES, + Moves.SWITCHEROO, + Moves.TECHNO_BLAST, + Moves.TERA_STARSTORM, + Moves.THIEF, + Moves.THOUSAND_ARROWS, + Moves.THOUSAND_WAVES, + Moves.THUNDER_CAGE, + Moves.THUNDEROUS_KICK, + Moves.TIDY_UP, + Moves.TRAILBLAZE, + Moves.TRANSFORM, + Moves.TRICK, + Moves.TWIN_BEAM, + Moves.V_CREATE, + Moves.WICKED_BLOW, + Moves.WICKED_TORQUE, + Moves.WIDE_GUARD, +]; + +const invalidAssistMoves: Moves[] = [ + Moves.ASSIST, + Moves.BANEFUL_BUNKER, + Moves.BEAK_BLAST, + Moves.BELCH, + Moves.BESTOW, + Moves.BOUNCE, + Moves.CELEBRATE, + Moves.CHATTER, + Moves.CIRCLE_THROW, + Moves.COPYCAT, + Moves.COUNTER, + Moves.COVET, + Moves.DESTINY_BOND, + Moves.DETECT, + Moves.DIG, + Moves.DIVE, + Moves.DRAGON_TAIL, + Moves.ENDURE, + Moves.FEINT, + Moves.FLY, + Moves.FOCUS_PUNCH, + Moves.FOLLOW_ME, + Moves.HELPING_HAND, + Moves.HOLD_HANDS, + Moves.KINGS_SHIELD, + Moves.MAT_BLOCK, + Moves.ME_FIRST, + Moves.METRONOME, + Moves.MIMIC, + Moves.MIRROR_COAT, + Moves.MIRROR_MOVE, + Moves.NATURE_POWER, + Moves.PHANTOM_FORCE, + Moves.PROTECT, + Moves.RAGE_POWDER, + Moves.ROAR, + Moves.SHADOW_FORCE, + Moves.SHELL_TRAP, + Moves.SKETCH, + Moves.SKY_DROP, + Moves.SLEEP_TALK, + Moves.SNATCH, + Moves.SPIKY_SHIELD, + Moves.SPOTLIGHT, + Moves.STRUGGLE, + Moves.SWITCHEROO, + Moves.THIEF, + Moves.TRANSFORM, + Moves.TRICK, + Moves.WHIRLWIND, +]; + +const invalidSleepTalkMoves: Moves[] = [ + Moves.ASSIST, + Moves.BELCH, + Moves.BEAK_BLAST, + Moves.BIDE, + Moves.BOUNCE, + Moves.COPYCAT, + Moves.DIG, + Moves.DIVE, + Moves.DYNAMAX_CANNON, + Moves.FREEZE_SHOCK, + Moves.FLY, + Moves.FOCUS_PUNCH, + Moves.GEOMANCY, + Moves.ICE_BURN, + Moves.ME_FIRST, + Moves.METRONOME, + Moves.MIRROR_MOVE, + Moves.MIMIC, + Moves.PHANTOM_FORCE, + Moves.RAZOR_WIND, + Moves.SHADOW_FORCE, + Moves.SHELL_TRAP, + Moves.SKETCH, + Moves.SKULL_BASH, + Moves.SKY_ATTACK, + Moves.SKY_DROP, + Moves.SLEEP_TALK, + Moves.SOLAR_BLADE, + Moves.SOLAR_BEAM, + Moves.STRUGGLE, + Moves.UPROAR, +]; + +const invalidCopycatMoves = [ + Moves.ASSIST, + Moves.BANEFUL_BUNKER, + Moves.BEAK_BLAST, + Moves.BEHEMOTH_BASH, + Moves.BEHEMOTH_BLADE, + Moves.BESTOW, + Moves.CELEBRATE, + Moves.CHATTER, + Moves.CIRCLE_THROW, + Moves.COPYCAT, + Moves.COUNTER, + Moves.COVET, + Moves.DESTINY_BOND, + Moves.DETECT, + Moves.DRAGON_TAIL, + Moves.ENDURE, + Moves.FEINT, + Moves.FOCUS_PUNCH, + Moves.FOLLOW_ME, + Moves.HELPING_HAND, + Moves.HOLD_HANDS, + Moves.KINGS_SHIELD, + Moves.MAT_BLOCK, + Moves.ME_FIRST, + Moves.METRONOME, + Moves.MIMIC, + Moves.MIRROR_COAT, + Moves.MIRROR_MOVE, + Moves.PROTECT, + Moves.RAGE_POWDER, + Moves.ROAR, + Moves.SHELL_TRAP, + Moves.SKETCH, + Moves.SLEEP_TALK, + Moves.SNATCH, + Moves.SPIKY_SHIELD, + Moves.SPOTLIGHT, + Moves.STRUGGLE, + Moves.SWITCHEROO, + Moves.THIEF, + Moves.TRANSFORM, + Moves.TRICK, + Moves.WHIRLWIND, +]; + export class NaturePowerAttr extends OverrideMoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { @@ -6686,45 +6984,29 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr { } } -const lastMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { - const copiableMove = user.scene.currentBattle.lastMove; - - if (!copiableMove) { - return false; - } - - if (allMoves[copiableMove].isChargingMove()) { - return false; +export class CopyMoveAttr extends CallMoveAttr { + private mirrorMove: boolean; + constructor(mirrorMove: boolean, invalidMoves: Moves[] = []) { + super(); + this.mirrorMove = mirrorMove; + this.invalidMoves = invalidMoves; } - // TODO: Add last turn of Bide - - return true; -}; - -export class CopyMoveAttr extends OverrideMoveEffectAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const lastMove = user.scene.currentBattle.lastMove; - - const moveTargets = getMoveTargets(user, lastMove); - if (!moveTargets.targets.length) { - return false; - } - - const targets = moveTargets.multiple || moveTargets.targets.length === 1 - ? moveTargets.targets - : moveTargets.targets.indexOf(target.getBattlerIndex()) > -1 - ? [ target.getBattlerIndex() ] - : [ moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; - user.getMoveQueue().push({ move: lastMove, targets: targets, ignorePP: true }); - - user.scene.unshiftPhase(new MovePhase(user.scene, user as PlayerPokemon, targets, new PokemonMove(lastMove, 0, 0, true), true)); - - return true; + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + this.hasTarget = this.mirrorMove; + const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : user.scene.currentBattle.lastMove; + return super.apply(user, target, allMoves[lastMove], args); } getCondition(): MoveConditionFunc { - return lastMoveCopiableCondition; + return (user, target, move) => { + if (this.mirrorMove) { + return target.getMoveHistory().length !== 0; + } else { + const lastMove = user.scene.currentBattle.lastMove; + return lastMove !== undefined && !this.invalidMoves.includes(lastMove); + } + }; } } @@ -7758,11 +8040,14 @@ export type MoveTargetSet = { multiple: boolean; }; -export function getMoveTargets(user: Pokemon, move: Moves): MoveTargetSet { +export function getMoveTargets(user: Pokemon, move: Moves, replaceTarget?: MoveTarget): MoveTargetSet { const variableTarget = new Utils.NumberHolder(0); user.getOpponents().forEach(p => applyMoveAttrs(VariableTargetAttr, user, p, allMoves[move], variableTarget)); - const moveTarget = allMoves[move].hasAttr(VariableTargetAttr) ? variableTarget.value : move ? allMoves[move].moveTarget : move === undefined ? MoveTarget.NEAR_ENEMY : []; + const moveTarget = allMoves[move].hasAttr(VariableTargetAttr) ? variableTarget.value : + replaceTarget !== undefined ? replaceTarget : + move ? allMoves[move].moveTarget : + move === undefined ? MoveTarget.NEAR_ENEMY : []; const opponents = user.getOpponents(); let set: Pokemon[] = []; @@ -7854,7 +8139,6 @@ export function initMoves() { .chargeText(i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" })) .attr(HighCritAttr) .windMove() - .ignoresVirtual() .target(MoveTarget.ALL_NEAR_ENEMIES), new SelfStatusMove(Moves.SWORDS_DANCE, Type.NORMAL, -1, 20, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.ATK ], 2, true) @@ -7873,8 +8157,7 @@ export function initMoves() { new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) .chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" })) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) - .condition(failOnGravityCondition) - .ignoresVirtual(), + .condition(failOnGravityCondition), new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) .attr(TrapAttr, BattlerTagType.BIND), new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1), @@ -8023,8 +8306,7 @@ export function initMoves() { new ChargingAttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) .chargeText(i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" })) .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]) - .attr(AntiSunlightPowerDecreaseAttr) - .ignoresVirtual(), + .attr(AntiSunlightPowerDecreaseAttr), new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) .powderMove(), @@ -8073,8 +8355,7 @@ export function initMoves() { .makesContact(false), new ChargingAttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) .chargeText(i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" })) - .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND) - .ignoresVirtual(), + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND), new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.TOXIC) .attr(ToxicAccuracyAttr), @@ -8098,8 +8379,7 @@ export function initMoves() { .attr(LevelDamageAttr), new StatusMove(Moves.MIMIC, Type.NORMAL, -1, 10, -1, 0, 1) .attr(MovesetCopyMoveAttr) - .ignoresSubstitute() - .ignoresVirtual(), + .ignoresSubstitute(), new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], -2) .soundBased(), @@ -8135,15 +8415,12 @@ export function initMoves() { new SelfStatusMove(Moves.FOCUS_ENERGY, Type.NORMAL, -1, 30, -1, 0, 1) .attr(AddBattlerTagAttr, BattlerTagType.CRIT_BOOST, true, true), new AttackMove(Moves.BIDE, Type.NORMAL, MoveCategory.PHYSICAL, -1, -1, 10, -1, 1, 1) - .ignoresVirtual() .target(MoveTarget.USER) .unimplemented(), new SelfStatusMove(Moves.METRONOME, Type.NORMAL, -1, 10, -1, 0, 1) - .attr(RandomMoveAttr) - .ignoresVirtual(), + .attr(RandomMoveAttr, invalidMetronomeMoves), new StatusMove(Moves.MIRROR_MOVE, Type.FLYING, -1, 20, -1, 0, 1) - .attr(CopyMoveAttr) - .ignoresVirtual(), + .attr(CopyMoveAttr, true), new AttackMove(Moves.SELF_DESTRUCT, Type.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1) .attr(SacrificialAttr) .makesContact(false) @@ -8171,8 +8448,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new ChargingAttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) .chargeText(i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" })) - .chargeAttr(StatStageChangeAttr, [ Stat.DEF ], 1, true) - .ignoresVirtual(), + .chargeAttr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1) .attr(MultiHitAttr) .makesContact(false), @@ -8212,8 +8488,7 @@ export function initMoves() { .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .attr(HighCritAttr) .attr(FlinchAttr) - .makesContact(false) - .ignoresVirtual(), + .makesContact(false), new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1) .attr(TransformAttr) // transforming from or into fusion pokemon causes various problems (such as crashes) @@ -8276,12 +8551,10 @@ export function initMoves() { new AttackMove(Moves.STRUGGLE, Type.NORMAL, MoveCategory.PHYSICAL, 50, -1, 1, -1, 0, 1) .attr(RecoilAttr, true, 0.25, true) .attr(TypelessAttr) - .ignoresVirtual() .target(MoveTarget.RANDOM_NEAR_ENEMY), new StatusMove(Moves.SKETCH, Type.NORMAL, -1, 1, -1, 0, 2) .ignoresSubstitute() - .attr(SketchAttr) - .ignoresVirtual(), + .attr(SketchAttr), new AttackMove(Moves.TRIPLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 10, 90, 10, -1, 0, 2) .attr(MultiHitAttr, MultiHitType._3) .attr(MultiHitPowerIncrementAttr, 3) @@ -8434,10 +8707,9 @@ export function initMoves() { .condition((user, target, move) => user.isOppositeGender(target)), new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2) .attr(BypassSleepAttr) - .attr(RandomMovesetMoveAttr) + .attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false) .condition(userSleptOrComatoseCondition) - .target(MoveTarget.ALL_ENEMIES) - .ignoresVirtual(), + .target(MoveTarget.NEAR_ENEMY), new StatusMove(Moves.HEAL_BELL, Type.NORMAL, -1, 5, -1, 0, 2) .attr(PartyStatusCureAttr, i18next.t("moveTriggers:bellChimed"), Abilities.SOUNDPROOF) .soundBased() @@ -8562,7 +8834,6 @@ export function initMoves() { .attr(FlinchAttr) .condition(new FirstMoveCondition()), new AttackMove(Moves.UPROAR, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 10, -1, 0, 3) - .ignoresVirtual() .soundBased() .target(MoveTarget.RANDOM_NEAR_ENEMY) .partial(), // Does not lock the user, does not stop Pokemon from sleeping @@ -8605,7 +8876,6 @@ export function initMoves() { new AttackMove(Moves.FOCUS_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3) .attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) })) .punchingMove() - .ignoresVirtual() .condition((user, target, move) => !user.turnData.attacksReceived.find(r => r.damage)), new AttackMove(Moves.SMELLING_SALTS, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1) @@ -8613,8 +8883,7 @@ export function initMoves() { new SelfStatusMove(Moves.FOLLOW_ME, Type.NORMAL, -1, 20, -1, 2, 3) .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true), new StatusMove(Moves.NATURE_POWER, Type.NORMAL, -1, 20, -1, 0, 3) - .attr(NaturePowerAttr) - .ignoresVirtual(), + .attr(NaturePowerAttr), new SelfStatusMove(Moves.CHARGE, Type.ELECTRIC, -1, 20, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1, true) .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), @@ -8635,8 +8904,7 @@ export function initMoves() { .triageMove() .attr(AddArenaTagAttr, ArenaTagType.WISH, 2, true), new SelfStatusMove(Moves.ASSIST, Type.NORMAL, -1, 20, -1, 0, 3) - .attr(RandomMovesetMoveAttr, true) - .ignoresVirtual(), + .attr(RandomMovesetMoveAttr, invalidAssistMoves, true), new SelfStatusMove(Moves.INGRAIN, Type.GRASS, -1, 20, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, true, true) @@ -8683,8 +8951,7 @@ export function initMoves() { new ChargingAttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) .chargeText(i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" })) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERWATER) - .chargeAttr(GulpMissileTagAttr) - .ignoresVirtual(), + .chargeAttr(GulpMissileTagAttr), new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3) .attr(MultiHitAttr), new SelfStatusMove(Moves.CAMOUFLAGE, Type.NORMAL, -1, 20, -1, 0, 3) @@ -8821,8 +9088,7 @@ export function initMoves() { .chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" })) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) - .condition(failOnGravityCondition) - .ignoresVirtual(), + .condition(failOnGravityCondition), new AttackMove(Moves.MUD_SHOT, Type.GROUND, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPD ], -1), new AttackMove(Moves.POISON_TAIL, Type.POISON, MoveCategory.PHYSICAL, 50, 100, 25, 10, 0, 3) @@ -8949,12 +9215,10 @@ export function initMoves() { .target(MoveTarget.USER_SIDE), new StatusMove(Moves.ME_FIRST, Type.NORMAL, -1, 20, -1, 0, 4) .ignoresSubstitute() - .ignoresVirtual() .target(MoveTarget.NEAR_ENEMY) .unimplemented(), new SelfStatusMove(Moves.COPYCAT, Type.NORMAL, -1, 20, -1, 0, 4) - .attr(CopyMoveAttr) - .ignoresVirtual(), + .attr(CopyMoveAttr, false, invalidCopycatMoves), new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4) .attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ]) .ignoresSubstitute(), @@ -9178,8 +9442,7 @@ export function initMoves() { new ChargingAttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" })) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) - .ignoresProtect() - .ignoresVirtual(), + .ignoresProtect(), new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true), new StatusMove(Moves.WIDE_GUARD, Type.ROCK, -1, 10, -1, 3, 5) @@ -9306,7 +9569,6 @@ export function initMoves() { .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .condition(failOnGravityCondition) .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) - .ignoresVirtual() .partial(), // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/ new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) @@ -9463,8 +9725,7 @@ export function initMoves() { .makesContact(false), new ChargingAttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5) .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" })) - .attr(StatusEffectAttr, StatusEffect.BURN) - .ignoresVirtual(), + .attr(StatusEffectAttr, StatusEffect.BURN), new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .soundBased() @@ -9507,8 +9768,7 @@ export function initMoves() { new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" })) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) - .ignoresProtect() - .ignoresVirtual(), + .ignoresProtect(), new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) .attr(AddTypeAttr, Type.GHOST), new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) @@ -9617,8 +9877,7 @@ export function initMoves() { .unimplemented(), new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) - .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) - .ignoresVirtual(), + .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() @@ -9685,116 +9944,79 @@ export function initMoves() { .ignoresProtect(), /* Unused */ new AttackMove(Moves.BREAKNECK_BLITZ__PHYSICAL, Type.NORMAL, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.BREAKNECK_BLITZ__SPECIAL, Type.NORMAL, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.ALL_OUT_PUMMELING__PHYSICAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.ALL_OUT_PUMMELING__SPECIAL, Type.FIGHTING, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SUPERSONIC_SKYSTRIKE__PHYSICAL, Type.FLYING, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SUPERSONIC_SKYSTRIKE__SPECIAL, Type.FLYING, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.ACID_DOWNPOUR__PHYSICAL, Type.POISON, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.ACID_DOWNPOUR__SPECIAL, Type.POISON, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.TECTONIC_RAGE__PHYSICAL, Type.GROUND, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.TECTONIC_RAGE__SPECIAL, Type.GROUND, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.CONTINENTAL_CRUSH__PHYSICAL, Type.ROCK, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.CONTINENTAL_CRUSH__SPECIAL, Type.ROCK, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SAVAGE_SPIN_OUT__PHYSICAL, Type.BUG, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SAVAGE_SPIN_OUT__SPECIAL, Type.BUG, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.NEVER_ENDING_NIGHTMARE__PHYSICAL, Type.GHOST, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.NEVER_ENDING_NIGHTMARE__SPECIAL, Type.GHOST, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.CORKSCREW_CRASH__PHYSICAL, Type.STEEL, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.CORKSCREW_CRASH__SPECIAL, Type.STEEL, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.INFERNO_OVERDRIVE__PHYSICAL, Type.FIRE, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.INFERNO_OVERDRIVE__SPECIAL, Type.FIRE, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.HYDRO_VORTEX__PHYSICAL, Type.WATER, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.HYDRO_VORTEX__SPECIAL, Type.WATER, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.BLOOM_DOOM__PHYSICAL, Type.GRASS, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.BLOOM_DOOM__SPECIAL, Type.GRASS, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.GIGAVOLT_HAVOC__PHYSICAL, Type.ELECTRIC, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.GIGAVOLT_HAVOC__SPECIAL, Type.ELECTRIC, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SHATTERED_PSYCHE__PHYSICAL, Type.PSYCHIC, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SHATTERED_PSYCHE__SPECIAL, Type.PSYCHIC, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SUBZERO_SLAMMER__PHYSICAL, Type.ICE, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SUBZERO_SLAMMER__SPECIAL, Type.ICE, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.DEVASTATING_DRAKE__PHYSICAL, Type.DRAGON, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.DEVASTATING_DRAKE__SPECIAL, Type.DRAGON, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.BLACK_HOLE_ECLIPSE__PHYSICAL, Type.DARK, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.BLACK_HOLE_ECLIPSE__SPECIAL, Type.DARK, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.TWINKLE_TACKLE__PHYSICAL, Type.FAIRY, MoveCategory.PHYSICAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.TWINKLE_TACKLE__SPECIAL, Type.FAIRY, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.CATASTROPIKA, Type.ELECTRIC, MoveCategory.PHYSICAL, 210, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), /* End Unused */ new SelfStatusMove(Moves.SHORE_UP, Type.GROUND, -1, 5, -1, 0, 7) .attr(SandHealAttr) @@ -9911,34 +10133,25 @@ export function initMoves() { /* Unused */ new AttackMove(Moves.SINISTER_ARROW_RAID, Type.GHOST, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) .makesContact(false) - .edgeCase() // I assume it's because the user needs spirit shackle and decidueye - .ignoresVirtual(), + .edgeCase(), // I assume it's because the user needs spirit shackle and decidueye new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) .attr(AlwaysHitMinimizeAttr) .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) - .edgeCase() // I assume it's because it needs darkest lariat and incineroar - .ignoresVirtual(), + .edgeCase(), // I assume it's because it needs darkest lariat and incineroar new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) - .edgeCase() // I assume it's because it needs sparkling aria and primarina - .ignoresVirtual(), + .edgeCase(), // I assume it's because it needs sparkling aria and primarina new AttackMove(Moves.GUARDIAN_OF_ALOLA, Type.FAIRY, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.SOUL_STEALING_7_STAR_STRIKE, Type.GHOST, MoveCategory.PHYSICAL, 195, -1, 1, -1, 0, 7) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.STOKED_SPARKSURFER, Type.ELECTRIC, MoveCategory.SPECIAL, 175, -1, 1, 100, 0, 7) - .edgeCase() // I assume it's because it needs thunderbolt and Alola Raichu - .ignoresVirtual(), + .edgeCase(), // I assume it's because it needs thunderbolt and Alola Raichu new AttackMove(Moves.PULVERIZING_PANCAKE, Type.NORMAL, MoveCategory.PHYSICAL, 210, -1, 1, -1, 0, 7) - .edgeCase() // I assume it's because it needs giga impact and snorlax - .ignoresVirtual(), + .edgeCase(), // I assume it's because it needs giga impact and snorlax new SelfStatusMove(Moves.EXTREME_EVOBOOST, Type.NORMAL, -1, 1, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) - .ignoresVirtual(), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true), new AttackMove(Moves.GENESIS_SUPERNOVA, Type.PSYCHIC, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) - .attr(TerrainChangeAttr, TerrainType.PSYCHIC) - .ignoresVirtual(), + .attr(TerrainChangeAttr, TerrainType.PSYCHIC), /* End Unused */ new AttackMove(Moves.SHELL_TRAP, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, -3, 7) .attr(AddBattlerTagHeaderAttr, BattlerTagType.SHELL_TRAP) @@ -9977,8 +10190,7 @@ export function initMoves() { .attr(FormChangeItemTypeAttr), /* Unused */ new AttackMove(Moves.TEN_MILLION_VOLT_THUNDERBOLT, Type.ELECTRIC, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) - .edgeCase() // I assume it's because it needs thunderbolt and pikachu in a cap - .ignoresVirtual(), + .edgeCase(), // I assume it's because it needs thunderbolt and pikachu in a cap /* End Unused */ new AttackMove(Moves.MIND_BLOWN, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 7) .condition(failIfDampCondition) @@ -9993,27 +10205,21 @@ export function initMoves() { /* Unused */ new AttackMove(Moves.LIGHT_THAT_BURNS_THE_SKY, Type.PSYCHIC, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) - .ignoresAbilities() - .ignoresVirtual(), + .ignoresAbilities(), new AttackMove(Moves.SEARING_SUNRAZE_SMASH, Type.STEEL, MoveCategory.PHYSICAL, 200, -1, 1, -1, 0, 7) - .ignoresAbilities() - .ignoresVirtual(), + .ignoresAbilities(), new AttackMove(Moves.MENACING_MOONRAZE_MAELSTROM, Type.GHOST, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7) - .ignoresAbilities() - .ignoresVirtual(), + .ignoresAbilities(), new AttackMove(Moves.LETS_SNUGGLE_FOREVER, Type.FAIRY, MoveCategory.PHYSICAL, 190, -1, 1, -1, 0, 7) - .edgeCase() // I assume it needs play rough and mimikyu - .ignoresVirtual(), + .edgeCase(), // I assume it needs play rough and mimikyu new AttackMove(Moves.SPLINTERED_STORMSHARDS, Type.ROCK, MoveCategory.PHYSICAL, 190, -1, 1, -1, 0, 7) .attr(ClearTerrainAttr) - .makesContact(false) - .ignoresVirtual(), + .makesContact(false), new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES) - .edgeCase() // I assume it needs clanging scales and Kommo-O - .ignoresVirtual(), + .edgeCase(), // I assume it needs clanging scales and Kommo-O /* End Unused */ new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) // LGPE Implementation .attr(CritOnlyAttr), @@ -10052,8 +10258,7 @@ export function initMoves() { /* Unused */ new SelfStatusMove(Moves.MAX_GUARD, Type.NORMAL, -1, 10, -1, 4, 8) .attr(ProtectAttr) - .condition(failIfLastCondition) - .ignoresVirtual(), + .condition(failIfLastCondition), /* End Unused */ new AttackMove(Moves.DYNAMAX_CANNON, Type.DRAGON, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8) .attr(MovePowerMultiplierAttr, (user, target, move) => { @@ -10066,8 +10271,7 @@ export function initMoves() { return 1; } }) - .attr(DiscourageFrequentUseAttr) - .ignoresVirtual(), + .attr(DiscourageFrequentUseAttr), new AttackMove(Moves.SNIPE_SHOT, Type.WATER, MoveCategory.SPECIAL, 80, 100, 15, -1, 0, 8) .attr(HighCritAttr) @@ -10112,76 +10316,58 @@ export function initMoves() { .attr(SwapArenaTagsAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.MIST, ArenaTagType.REFLECT, ArenaTagType.SPIKES, ArenaTagType.STEALTH_ROCK, ArenaTagType.STICKY_WEB, ArenaTagType.TAILWIND, ArenaTagType.TOXIC_SPIKES ]), new AttackMove(Moves.MAX_FLARE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_FLUTTERBY, Type.BUG, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_LIGHTNING, Type.ELECTRIC, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_STRIKE, Type.NORMAL, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_KNUCKLE, Type.FIGHTING, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_PHANTASM, Type.GHOST, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_HAILSTORM, Type.ICE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_OOZE, Type.POISON, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_GEYSER, Type.WATER, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_AIRSTREAM, Type.FLYING, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_STARFALL, Type.FAIRY, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_WYRMWIND, Type.DRAGON, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_MINDSTORM, Type.PSYCHIC, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_ROCKFALL, Type.ROCK, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_QUAKE, Type.GROUND, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_DARKNESS, Type.DARK, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_OVERGROWTH, Type.GRASS, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), new AttackMove(Moves.MAX_STEELSPIKE, Type.STEEL, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.NEAR_ENEMY) - .unimplemented() - .ignoresVirtual(), + .unimplemented(), /* End Unused */ new SelfStatusMove(Moves.CLANGOROUS_SOUL, Type.DRAGON, 100, 5, -1, 0, 8) .attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, 3) @@ -10254,8 +10440,7 @@ export function initMoves() { .makesContact(false), new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) .chargeText(i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" })) - .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) - .ignoresVirtual(), + .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true), new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) .attr(ShellSideArmCategoryAttr) .attr(StatusEffectAttr, StatusEffect.POISON) @@ -10713,8 +10898,7 @@ export function initMoves() { new ChargingAttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9) .chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" })) .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) - .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ]) - .ignoresVirtual(), + .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ]), new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(TeraMoveCategoryAttr) .attr(TeraStarstormTypeAttr) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6aa4bd46a68..5a6fa83eb0f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3286,7 +3286,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - getMoveQueue(): QueuedMove[] { + getMoveQueue(): TurnMove[] { return this.summonData.moveQueue; } @@ -4792,17 +4792,19 @@ export class EnemyPokemon extends Pokemon { * the Pokemon the move will target. * @returns this Pokemon's next move in the format {move, moveTargets} */ - getNextMove(): QueuedMove { + getNextMove(): TurnMove { // If this Pokemon has a move already queued, return it. - const queuedMove = this.getMoveQueue().length - ? this.getMoveset().find(m => m?.moveId === this.getMoveQueue()[0].move) - : null; - if (queuedMove) { - if (queuedMove.isUsable(this, this.getMoveQueue()[0].ignorePP)) { - return { move: queuedMove.moveId, targets: this.getMoveQueue()[0].targets, ignorePP: this.getMoveQueue()[0].ignorePP }; - } else { - this.getMoveQueue().shift(); - return this.getNextMove(); + const moveQueue = this.getMoveQueue(); + if (moveQueue.length !== 0) { + const queuedMove = moveQueue[0]; + if (queuedMove) { + const moveIndex = this.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move); + if ((moveIndex > -1 && this.getMoveset()[moveIndex]!.isUsable(this, queuedMove.ignorePP)) || queuedMove.virtual) { + return queuedMove; + } else { + this.getMoveQueue().shift(); + return this.getNextMove(); + } } } @@ -5224,15 +5226,10 @@ export class EnemyPokemon extends Pokemon { export interface TurnMove { move: Moves; - targets?: BattlerIndex[]; - result: MoveResult; + targets: BattlerIndex[]; + result?: MoveResult; virtual?: boolean; turn?: number; -} - -export interface QueuedMove { - move: Moves; - targets: BattlerIndex[]; ignorePP?: boolean; } @@ -5248,7 +5245,7 @@ export interface AttackMoveResult { export class PokemonSummonData { /** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */ public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ]; - public moveQueue: QueuedMove[] = []; + public moveQueue: TurnMove[] = []; public tags: BattlerTag[] = []; public abilitySuppressed: boolean = false; public abilitiesApplied: Abilities[] = []; diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index eab76282908..f96a738431b 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -8,7 +8,7 @@ import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Biome } from "#app/enums/biome"; import { Moves } from "#app/enums/moves"; import { PokeballType } from "#enums/pokeball"; -import { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; +import { FieldPosition, PlayerPokemon, TurnMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { Command } from "#app/ui/command-ui-handler"; import { Mode } from "#app/ui/ui"; @@ -76,19 +76,19 @@ export class CommandPhase extends FieldPhase { const moveQueue = playerPokemon.getMoveQueue(); while (moveQueue.length && moveQueue[0] - && moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move) + && moveQueue[0].move && !moveQueue[0].virtual && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move) || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move)]!.isUsable(playerPokemon, moveQueue[0].ignorePP))) { // TODO: is the bang correct? moveQueue.shift(); } - if (moveQueue.length) { + if (moveQueue.length !== 0) { const queuedMove = moveQueue[0]; if (!queuedMove.move) { - this.handleCommand(Command.FIGHT, -1, false); + this.handleCommand(Command.FIGHT, -1, undefined); } else { const moveIndex = playerPokemon.getMoveset().findIndex(m => m?.moveId === queuedMove.move); - if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) { // TODO: is the bang correct? - this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1 }); + if ((moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) || queuedMove.virtual) { // TODO: is the bang correct? + this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, queuedMove); } else { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); } @@ -110,12 +110,24 @@ export class CommandPhase extends FieldPhase { switch (command) { case Command.FIGHT: let useStruggle = false; + const turnMove: TurnMove | undefined = (args.length === 2 ? (args[1] as TurnMove) : undefined); if (cursor === -1 || playerPokemon.trySelectMove(cursor, args[0] as boolean) || (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m?.isUsable(playerPokemon)).length)) { - const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor]!.moveId : Moves.NONE : Moves.STRUGGLE; // TODO: is the bang correct? + + let moveId: Moves; + if (useStruggle) { + moveId = Moves.STRUGGLE; + } else if (turnMove !== undefined) { + moveId = turnMove.move; + } else if (cursor > -1) { + moveId = playerPokemon.getMoveset()[cursor]!.moveId; + } else { + moveId = Moves.NONE; + } + const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args }; - const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2]; + const moveTargets: MoveTargetSet = turnMove === undefined ? getMoveTargets(playerPokemon, moveId) : { targets: turnMove.targets, multiple: turnMove.targets.length > 1 }; if (!moveId) { turnCommand.targets = [ this.fieldIndex ]; } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 005cdbe1716..3f8084d518a 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -294,11 +294,6 @@ export class MovePhase extends BattlePhase { this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed)); } - // Update the battle's "last move" pointer, unless we're currently mimicking a move. - if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) { - this.scene.currentBattle.lastMove = this.move.moveId; - } - /** * Determine if the move is successful (meaning that its damage/effects can be attempted) * by checking that all of the following are true: @@ -322,6 +317,14 @@ export class MovePhase extends BattlePhase { const success = passesConditions && !failedDueToWeather && !failedDueToTerrain; + // Update the battle's "last move" pointer, unless we're currently mimicking a move. + if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) { + // The last move used is unaffected by moves that fail + if (success) { + this.scene.currentBattle.lastMove = this.move.moveId; + } + } + /** * If the move has not failed, trigger ability-based user type changes and then execute it. * @@ -516,7 +519,7 @@ export class MovePhase extends BattlePhase { frenzyMissFunc(this.pokemon, this.move.getMove()); } - this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); + this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL, targets: this.targets }); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); diff --git a/src/test/moves/assist.test.ts b/src/test/moves/assist.test.ts new file mode 100644 index 00000000000..0e665cabd74 --- /dev/null +++ b/src/test/moves/assist.test.ts @@ -0,0 +1,70 @@ +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#app/enums/stat"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Assist", () => { + 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 + .moveset([ Moves.ASSIST, Moves.SKETCH, Moves.FLY, Moves.DRAGON_TAIL ]) // These are all moves Assist cannot call; Sketch will be used to test that it can call other moves properly + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should be able to use an ally's moves", async () => { + game.override + .battleType("double") + .enemyMoveset(Moves.SWORDS_DANCE); + await game.classicMode.startBattle([ Species.FEEBAS, Species.SHUCKLE ]); + + game.move.select(Moves.ASSIST, 0); + game.move.select(Moves.SKETCH, 1); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER ]); + // Player_2 uses Sketch, copies Swords Dance, Player_1 uses Assist, uses Player_2's Sketched Swords Dance + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(2); // Stat raised from Assist -> Swords Dance + }); + + it("should fail if there are no usable moves", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + // Moves above are already unusable by Assist + game.move.select(Moves.ASSIST, 0); + await game.toNextTurn(); + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should apply secondary effects of a move", async () => { + game.override.moveset([ Moves.ASSIST, Moves.WOOD_HAMMER, Moves.WOOD_HAMMER, Moves.WOOD_HAMMER ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ASSIST, 0); + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // should receive recoil damage from Wood Hammer + }); +}); diff --git a/src/test/moves/copycat.test.ts b/src/test/moves/copycat.test.ts new file mode 100644 index 00000000000..60f9c5e3f68 --- /dev/null +++ b/src/test/moves/copycat.test.ts @@ -0,0 +1,88 @@ +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#app/enums/stat"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Copycat", () => { + 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 + .moveset([ Moves.COPYCAT, Moves.SPIKY_SHIELD, Moves.SWORDS_DANCE, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .starterSpecies(Species.FEEBAS) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should copy the last move successfully executed", async () => { + game.override.enemyMoveset(Moves.SUCKER_PUNCH); + await game.classicMode.startBattle(); + + game.move.select(Moves.SWORDS_DANCE); + await game.toNextTurn(); + + game.move.select(Moves.COPYCAT); // Last successful move should be Swords Dance + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(4); + }); + + it("should fail when the last move used is not a valid Copycat move", async () => { + game.override.enemyMoveset(Moves.PROTECT); // Protect is not a valid move for Copycat to copy + await game.classicMode.startBattle(); + + game.move.select(Moves.SPIKY_SHIELD); // Spiky Shield is not a valid move for Copycat to copy + await game.toNextTurn(); + + game.move.select(Moves.COPYCAT); + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should copy the called move when the last move successfully calls another", async () => { + game.override + .moveset([ Moves.SPLASH, Moves.METRONOME ]) + .enemyMoveset(Moves.COPYCAT); + await game.classicMode.startBattle(); + vi.spyOn(game.scene.getPlayerPokemon()!, "randSeedInt").mockReturnValue(Moves.SWORDS_DANCE); + + game.move.select(Moves.METRONOME); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); // Player moves first, so enemy can copy Swords Dance + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(2); + }); + + it("should apply secondary effects of a move", async () => { + game.override.enemyMoveset(Moves.ACID_SPRAY); // Secondary effect lowers SpDef by 2 stages + await game.classicMode.startBattle(); + + game.move.select(Moves.COPYCAT); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2); + }); +}); diff --git a/src/test/moves/metronome.test.ts b/src/test/moves/metronome.test.ts new file mode 100644 index 00000000000..7f1d9777327 --- /dev/null +++ b/src/test/moves/metronome.test.ts @@ -0,0 +1,117 @@ +import { RechargingTag, SemiInvulnerableTag } from "#app/data/battler-tags"; +import { Abilities } from "#app/enums/abilities"; +import { Stat } from "#app/enums/stat"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Moves - Metronome", () => { + 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 + .moveset([ Moves.METRONOME, Moves.SPLASH ]) + .battleType("single") + .startingLevel(100) + .starterSpecies(Species.REGIELEKI) + .enemyLevel(100) + .enemySpecies(Species.SHUCKLE) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH); + }); + + it("should have one semi-invulnerable turn and deal damage on the second turn when a semi-invulnerable move is called", async () => { + await game.classicMode.startBattle(); + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.DIVE); + + game.move.select(Moves.METRONOME); + await game.toNextTurn(); + + expect(player.getTag(SemiInvulnerableTag)).toBeTruthy(); + await game.move.forceHit(); // Force hit on Dive, required due to randSeedInt mock making hitCheck return false every time. + + await game.toNextTurn(); + expect(player.getTag(SemiInvulnerableTag)).toBeFalsy(); + expect(enemy.isFullHp()).toBeFalsy(); + }); + + it("should apply secondary effects of a move", async () => { + await game.classicMode.startBattle(); + const player = game.scene.getPlayerPokemon()!; + vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.WOOD_HAMMER); + + game.move.select(Moves.METRONOME); + await game.phaseInterceptor.to("MoveEffectPhase"); // Metronome has its own MoveEffectPhase, followed by Wood Hammer's MoveEffectPhase + await game.move.forceHit(); // Calls forceHit on Wood Hammer's MoveEffectPhase, required due to randSeedInt mock making hitCheck return false every time. + await game.toNextTurn(); + + expect(player.isFullHp()).toBeFalsy(); + }); + + it("should recharge after using recharge move", async () => { + await game.classicMode.startBattle(); + const player = game.scene.getPlayerPokemon()!; + vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.HYPER_BEAM); + + game.move.select(Moves.METRONOME); + await game.phaseInterceptor.to("MoveEffectPhase"); + await game.move.forceHit(); + await game.toNextTurn(); + + expect(player.getTag(RechargingTag)).toBeTruthy(); + }); + + it("should only target ally for Aromatic Mist", async () => { + game.override.battleType("double"); + await game.classicMode.startBattle([ Species.REGIELEKI, Species.RATTATA ]); + const [ leftPlayer, rightPlayer ] = game.scene.getPlayerField(); + const [ leftOpp, rightOpp ] = game.scene.getEnemyField(); + vi.spyOn(leftPlayer, "randSeedInt").mockReturnValue(Moves.AROMATIC_MIST); + + game.move.select(Moves.METRONOME); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("MoveEffectPhase"); + await game.move.forceHit(); + await game.toNextTurn(); + + expect(rightPlayer.getStatStage(Stat.SPDEF)).toBe(1); + expect(leftPlayer.getStatStage(Stat.SPDEF)).toBe(0); + expect(leftOpp.getStatStage(Stat.SPDEF)).toBe(0); + expect(rightOpp.getStatStage(Stat.SPDEF)).toBe(0); + }); + + it("should cause opponent to flee, and not crash for Roar", async () => { + await game.classicMode.startBattle(); + const player = game.scene.getPlayerPokemon()!; + vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.ROAR); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.METRONOME); + await game.phaseInterceptor.to("MoveEffectPhase"); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase"); + + const isVisible = enemyPokemon.visible; + const hasFled = enemyPokemon.switchOutStatus; + expect(!isVisible && hasFled).toBe(true); + + await game.phaseInterceptor.to("BattleEndPhase"); + }); +}); diff --git a/src/test/moves/mirror_move.test.ts b/src/test/moves/mirror_move.test.ts new file mode 100644 index 00000000000..7b84762948a --- /dev/null +++ b/src/test/moves/mirror_move.test.ts @@ -0,0 +1,84 @@ +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#app/enums/stat"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Mirror Move", () => { + 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 + .moveset([ Moves.MIRROR_MOVE, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should use the last move that the target", async () => { + game.override + .battleType("double") + .enemyMoveset([ Moves.TACKLE, Moves.GROWL ]); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + game.move.select(Moves.MIRROR_MOVE, 0, BattlerIndex.ENEMY); // target's last move is Tackle, enemy should receive damage from Mirror Move copying Tackle + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.GROWL, BattlerIndex.PLAYER_2); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getEnemyField()[0].isFullHp()).toBeFalsy(); + }); + + it("should apply secondary effects of a move", async () => { + game.override.enemyMoveset(Moves.ACID_SPRAY); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.MIRROR_MOVE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2); + }); + + it("should be able to copy status moves", async () => { + game.override.enemyMoveset(Moves.GROWL); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.MIRROR_MOVE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should fail if the target has not used any moves", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.MIRROR_MOVE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); +}); diff --git a/src/test/moves/sleep_talk.test.ts b/src/test/moves/sleep_talk.test.ts new file mode 100644 index 00000000000..3c001783b2c --- /dev/null +++ b/src/test/moves/sleep_talk.test.ts @@ -0,0 +1,75 @@ +import { Stat } from "#app/enums/stat"; +import { StatusEffect } from "#app/enums/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Sleep Talk", () => { + 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 + .moveset([ Moves.SPLASH, Moves.SLEEP_TALK ]) + .statusEffect(StatusEffect.SLEEP) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .enemyLevel(100); + }); + + it("should fail when the user is not asleep", async () => { + game.override.statusEffect(StatusEffect.NONE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SLEEP_TALK); + await game.toNextTurn(); + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if the user has no valid moves", async () => { + game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.METRONOME, Moves.SOLAR_BEAM, Moves.SLEEP_TALK ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SLEEP_TALK); + await game.toNextTurn(); + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should call a random valid move if the user is asleep", async () => { + game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.SWORDS_DANCE ]); // Dig and Fly are invalid moves, Swords Dance should always be called + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SLEEP_TALK); + await game.toNextTurn(); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)); + }); + + it("should apply secondary effects of a move", async () => { + game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.WOOD_HAMMER ]); // Dig and Fly are invalid moves, Wood Hammer should always be called + await game.classicMode.startBattle(); + + game.move.select(Moves.SLEEP_TALK); + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // Wood Hammer recoil effect should be applied + }); +}); diff --git a/src/test/moves/spit_up.test.ts b/src/test/moves/spit_up.test.ts index 8e418858e8d..96511b3bdfb 100644 --- a/src/test/moves/spit_up.test.ts +++ b/src/test/moves/spit_up.test.ts @@ -124,7 +124,7 @@ describe("Moves - Spit Up", () => { game.move.select(Moves.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.FAIL }); + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.FAIL, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]}); expect(spitUp.calculateBattlePower).not.toHaveBeenCalled(); }); @@ -147,7 +147,7 @@ describe("Moves - Spit Up", () => { await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS }); + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]}); expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); @@ -175,7 +175,7 @@ describe("Moves - Spit Up", () => { game.move.select(Moves.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS }); + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]}); expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); diff --git a/src/test/moves/stockpile.test.ts b/src/test/moves/stockpile.test.ts index d3239856ed7..3271a12e02f 100644 --- a/src/test/moves/stockpile.test.ts +++ b/src/test/moves/stockpile.test.ts @@ -71,7 +71,7 @@ describe("Moves - Stockpile", () => { expect(user.getStatStage(Stat.SPDEF)).toBe(3); expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(3); - expect(user.getMoveHistory().at(-1)).toMatchObject({ result: MoveResult.FAIL, move: Moves.STOCKPILE }); + expect(user.getMoveHistory().at(-1)).toMatchObject({ result: MoveResult.FAIL, move: Moves.STOCKPILE, targets: [ user.getBattlerIndex() ]}); } } }); diff --git a/src/test/moves/swallow.test.ts b/src/test/moves/swallow.test.ts index 2aee4d2604a..786cdbd9b4c 100644 --- a/src/test/moves/swallow.test.ts +++ b/src/test/moves/swallow.test.ts @@ -134,7 +134,7 @@ describe("Moves - Swallow", () => { game.move.select(Moves.SWALLOW); await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.FAIL }); + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.FAIL, targets: [ pokemon.getBattlerIndex() ]}); }); describe("restores stat stage boosts granted by stacks", () => { @@ -155,7 +155,7 @@ describe("Moves - Swallow", () => { await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.SUCCESS }); + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.SUCCESS, targets: [ pokemon.getBattlerIndex() ]}); expect(pokemon.getStatStage(Stat.DEF)).toBe(0); expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0); @@ -182,7 +182,7 @@ describe("Moves - Swallow", () => { await game.phaseInterceptor.to(TurnInitPhase); - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.SUCCESS }); + expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.SUCCESS, targets: [ pokemon.getBattlerIndex() ]}); expect(pokemon.getStatStage(Stat.DEF)).toBe(1); expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2);