Fixed up move-copying moves; added Natural Gift move usage text

This commit is contained in:
Bertie690 2025-06-12 17:22:24 -04:00
parent ff9aefb0e5
commit 3413d075e9
8 changed files with 453 additions and 348 deletions

View File

@ -1,6 +1,6 @@
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
/** Set of moves that cannot be called by {@linkcode MoveId.METRONOME Metronome} */ /** Set of moves that cannot be called by {@linkcode MoveId.METRONOME | Metronome}. */
export const invalidMetronomeMoves: ReadonlySet<MoveId> = new Set([ export const invalidMetronomeMoves: ReadonlySet<MoveId> = new Set([
MoveId.AFTER_YOU, MoveId.AFTER_YOU,
MoveId.ASSIST, MoveId.ASSIST,

View File

@ -3092,7 +3092,6 @@ export class OverrideMoveEffectAttr extends MoveAttr {
/** /**
* Attack Move that doesn't hit the turn it is played and doesn't allow for multiple * Attack Move that doesn't hit the turn it is played and doesn't allow for multiple
* uses on the same target. Examples are Future Sight or Doom Desire. * uses on the same target. Examples are Future Sight or Doom Desire.
* @extends OverrideMoveEffectAttr
* @param tagType The {@linkcode ArenaTagType} that will be placed on the field when the move is used * @param tagType The {@linkcode ArenaTagType} that will be placed on the field when the move is used
* @param chargeAnim The {@linkcode ChargeAnim | Charging Animation} used for the move * @param chargeAnim The {@linkcode ChargeAnim | Charging Animation} used for the move
* @param chargeText The text to display when the move is used * @param chargeText The text to display when the move is used
@ -3137,7 +3136,6 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
/** /**
* Attribute that cancels the associated move's effects when set to be combined with the user's ally's * Attribute that cancels the associated move's effects when set to be combined with the user's ally's
* subsequent move this turn. Used for Grass Pledge, Water Pledge, and Fire Pledge. * subsequent move this turn. Used for Grass Pledge, Water Pledge, and Fire Pledge.
* @extends OverrideMoveEffectAttr
*/ */
export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
constructor() { constructor() {
@ -6751,68 +6749,219 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
} }
/** /**
* Attribute used to call a move. * Abstract attribute used for all move-calling moves, containing common functionality
* Used by other move attributes: {@linkcode RandomMoveAttr}, {@linkcode RandomMovesetMoveAttr}, {@linkcode CopyMoveAttr} * for executing called moves.
* @see {@linkcode apply} for move call
* @extends OverrideMoveEffectAttr
*/ */
class CallMoveAttr extends OverrideMoveEffectAttr { export abstract class CallMoveAttr extends OverrideMoveEffectAttr {
protected invalidMoves: ReadonlySet<MoveId>; constructor(
protected hasTarget: boolean; /**
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { * Whether this move should target the user; default `true`.
const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined; * If `true`, will unleash non-spread moves against a random eligible target,
const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget); * or else the move's selected target.
if (moveTargets.targets.length === 0) { */
globalScene.phaseManager.queueMessage(i18next.t("battle:attackFailed")); override selfTarget = true,
console.log("CallMoveAttr failed due to no targets."); ) {
return false; super(selfTarget)
} }
/**
* Abstract function yielding the move to be used.
* @param user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} being targeted by the move
* @returns The MoveId that will be called and used.
*/
protected abstract getMove(user: Pokemon, target: Pokemon): MoveId;
override apply(user: Pokemon, target: Pokemon): boolean {
const copiedMove = allMoves[this.getMove(user, target)];
const replaceMoveTarget = copiedMove.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined;
const moveTargets = getMoveTargets(user, copiedMove.id, replaceMoveTarget);
const targets = moveTargets.multiple || moveTargets.targets.length === 1 const targets = moveTargets.multiple || moveTargets.targets.length === 1
? moveTargets.targets ? moveTargets.targets
: [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already : [ this.selfTarget ? target.getBattlerIndex() : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already
user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true }); user.getMoveQueue().push({ move: copiedMove.id, targets: targets, virtual: true, ignorePP: true });
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id); globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", copiedMove.id);
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id, 0, 0, true), true, true); globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(copiedMove.id, 0, 0, true), true, true);
return true; return true;
} }
} }
/** /**
* Attribute used to call a random move. * Attribute to call a different move based on the current terrain and biome.
* Used for {@linkcode MoveId.METRONOME} * Used by {@linkcode MoveId.NATURE_POWER}
* @see {@linkcode apply} for move selection and move call */
* @extends CallMoveAttr to call a selected move export class NaturePowerAttr extends CallMoveAttr {
constructor() {
super(false)
}
override getMove(user: Pokemon): MoveId {
const moveId = this.getMoveIdForTerrain(globalScene.arena.getTerrainType(), globalScene.arena.biomeType)
// Unshift a phase to load the move's animation (in case it isn't already), then use the move.
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:naturePowerUse", {
pokemonName: getPokemonNameWithAffix(user),
moveName: allMoves[moveId].name,
}))
return moveId;
}
/**
* Helper function to retrieve the correct move for the current terrain and biome.
* Made into a separate function for brevity.
*/
private getMoveIdForTerrain(terrain: TerrainType, biome: BiomeId): MoveId {
switch (terrain) {
case TerrainType.ELECTRIC:
return MoveId.THUNDERBOLT;
case TerrainType.GRASSY:
return MoveId.ENERGY_BALL;
case TerrainType.PSYCHIC:
return MoveId.PSYCHIC;
case TerrainType.MISTY:
return MoveId.MOONBLAST;
}
// No terrain means check biome
switch (biome) {
case BiomeId.TOWN:
return MoveId.ROUND;
case BiomeId.METROPOLIS:
return MoveId.TRI_ATTACK;
case BiomeId.SLUM:
return MoveId.SLUDGE_BOMB;
case BiomeId.PLAINS:
return MoveId.SILVER_WIND;
case BiomeId.GRASS:
return MoveId.GRASS_KNOT;
case BiomeId.TALL_GRASS:
return MoveId.POLLEN_PUFF;
case BiomeId.MEADOW:
return MoveId.GIGA_DRAIN;
case BiomeId.FOREST:
return MoveId.BUG_BUZZ;
case BiomeId.JUNGLE:
return MoveId.LEAF_STORM;
case BiomeId.SEA:
return MoveId.HYDRO_PUMP;
case BiomeId.SWAMP:
return MoveId.MUD_BOMB;
case BiomeId.BEACH:
return MoveId.SCALD;
case BiomeId.LAKE:
return MoveId.BUBBLE_BEAM;
case BiomeId.SEABED:
return MoveId.BRINE;
case BiomeId.ISLAND:
return MoveId.LEAF_TORNADO;
case BiomeId.MOUNTAIN:
return MoveId.AIR_SLASH;
case BiomeId.BADLANDS:
return MoveId.EARTH_POWER;
case BiomeId.DESERT:
return MoveId.SCORCHING_SANDS;
case BiomeId.WASTELAND:
return MoveId.DRAGON_PULSE;
case BiomeId.CONSTRUCTION_SITE:
return MoveId.STEEL_BEAM;
case BiomeId.CAVE:
return MoveId.POWER_GEM;
case BiomeId.ICE_CAVE:
return MoveId.ICE_BEAM;
case BiomeId.SNOWY_FOREST:
return MoveId.FROST_BREATH;
case BiomeId.VOLCANO:
return MoveId.LAVA_PLUME;
case BiomeId.GRAVEYARD:
return MoveId.SHADOW_BALL;
case BiomeId.RUINS:
return MoveId.ANCIENT_POWER;
case BiomeId.TEMPLE:
return MoveId.EXTRASENSORY;
case BiomeId.DOJO:
return MoveId.FOCUS_BLAST;
case BiomeId.FAIRY_CAVE:
return MoveId.ALLURING_VOICE;
case BiomeId.ABYSS:
return MoveId.OMINOUS_WIND;
case BiomeId.SPACE:
return MoveId.DRACO_METEOR;
case BiomeId.FACTORY:
return MoveId.FLASH_CANNON;
case BiomeId.LABORATORY:
return MoveId.ZAP_CANNON;
case BiomeId.POWER_PLANT:
return MoveId.CHARGE_BEAM;
case BiomeId.END:
return MoveId.ETERNABEAM;
default:
// Fallback for no match
console.log(`NaturePowerAttr lacks defined move to use for current biome ${toReadableString(BiomeId[globalScene.arena.biomeType])}; consider adding an appropriate move to the attribute's selection table.`)
return MoveId.TRI_ATTACK;
}
}
}
/**
* Attribute used to copy a previously-used move.
* Used for {@linkcode MoveId.COPYCAT} and {@linkcode MoveId.MIRROR_MOVE}.
*/
export class CopyMoveAttr extends CallMoveAttr {
constructor(
/**
* A {@linkcode ReadonlySet} containing all moves that this MoveAttr cannot copy,
* in addition to unimplemented moves and `MoveId.NONE`.
* The move will fail if the chosen move is inside this banlist (if it exists).
*/
protected readonly invalidMoves: ReadonlySet<MoveId>,
selfTarget = true,
) {
super(selfTarget);
}
override getMove(_user: Pokemon, target: Pokemon): MoveId {
return this.selfTarget
? target.getLastXMoves()[0]?.move ?? MoveId.NONE
: globalScene.currentBattle.lastMove;
}
getCondition(): MoveConditionFunc {
return (_user, target, _move) => {
const chosenMove = this.getMove(_user, target);
return chosenMove !== MoveId.NONE && !this.invalidMoves.has(chosenMove);
};
}
}
/**
* Attribute to call a random move among moves not in a banlist.
* Used for {@linkcode MoveId.METRONOME}.
*/ */
export class RandomMoveAttr extends CallMoveAttr { export class RandomMoveAttr extends CallMoveAttr {
constructor(invalidMoves: ReadonlySet<MoveId>) { constructor(
super();
this.invalidMoves = invalidMoves;
}
/** /**
* This function exists solely to allow tests to override the randomly selected move by mocking this function. * A {@linkcode ReadonlySet} containing all moves that this MoveAttr cannot copy,
* in addition to unimplemented moves and `MoveId.NONE`.
* The move will fail if the chosen move is inside this banlist (if it exists).
*/ */
public getMoveOverride(): MoveId | null { protected readonly invalidMoves: ReadonlySet<MoveId>,
return null; ) {
super(true);
} }
/** /**
* User calls a random moveId. * Pick a random move to execute, barring unimplemented moves and ones
* in this move's {@linkcode invalidMetronomeMoves | exclusion list}.
* Overridden as public to allow tests to override move choice using mocks.
* *
* Invalid moves are indicated by what is passed in to invalidMoves: {@linkcode invalidMetronomeMoves} * @param user - The {@linkcode Pokemon} using the move
* @param user Pokemon that used the move and will call a random move * @returns The {@linkcode MoveId} that will be called.
* @param target Pokemon that will be targeted by the random move (if single target)
* @param move Move being used
* @param args Unused
*/ */
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { public override getMove(user: Pokemon): MoveId {
const moveIds = getEnumValues(MoveId).map(m => !this.invalidMoves.has(m) && !allMoves[m].name.endsWith(" (N)") ? m : MoveId.NONE); const moveIds = getEnumValues(MoveId).filter(m => m !== MoveId.NONE && !this.invalidMoves.has(m) && !allMoves[m].name.endsWith(" (N)"));
let moveId: MoveId = MoveId.NONE; return moveIds[user.randBattleSeedInt(moveIds.length)];
do {
moveId = this.getMoveOverride() ?? moveIds[user.randBattleSeedInt(moveIds.length)];
}
while (moveId === MoveId.NONE);
return super.apply(user, target, allMoves[moveId], args);
} }
} }
@ -6821,221 +6970,51 @@ export class RandomMoveAttr extends CallMoveAttr {
* Used for {@linkcode MoveId.ASSIST} and {@linkcode MoveId.SLEEP_TALK} * Used for {@linkcode MoveId.ASSIST} and {@linkcode MoveId.SLEEP_TALK}
* *
* Fails if the user has no callable moves. * Fails if the user has no callable moves.
*
* Invalid moves are indicated by what is passed in to invalidMoves: {@linkcode invalidAssistMoves} or {@linkcode invalidSleepTalkMoves}
* @extends RandomMoveAttr to use the callMove function on a moveId
* @see {@linkcode getCondition} for move selection
*/ */
export class RandomMovesetMoveAttr extends CallMoveAttr { export class RandomMovesetMoveAttr extends RandomMoveAttr {
private includeParty: boolean; /**
private moveId: number; * The previously-selected MoveId for this attribute.
constructor(invalidMoves: ReadonlySet<MoveId>, includeParty: boolean = false) { * Reset to `MoveId.NONE` after successful use.
super(); */
this.includeParty = includeParty; private selectedMove: MoveId = MoveId.NONE
this.invalidMoves = invalidMoves; constructor(invalidMoves: ReadonlySet<MoveId>,
/**
* Whether to consider all moves from the user's party (`true`) or the user's own moveset (`false`);
* default `false`.
*/
private includeParty = false
) {
super(invalidMoves);
} }
/** /**
* User calls a random moveId selected in {@linkcode getCondition} * Select a random move from either the user's or its party members' movesets,
* @param user Pokemon that used the move and will call a random move * or return an already-selected one if one exists.
* @param target Pokemon that will be targeted by the random move (if single target) *
* @param move Move being used * @param user - The {@linkcode Pokemon} using the move.
* @param args Unused * @returns The {@linkcode MoveId} that will be called.
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { override getMove(user: Pokemon): MoveId {
return super.apply(user, target, allMoves[this.moveId], args); if (this.selectedMove) {
const m = this.selectedMove;
this.selectedMove = MoveId.NONE;
return m;
} }
getCondition(): MoveConditionFunc {
return (user, target, move) => {
// includeParty will be true for Assist, false for Sleep Talk // includeParty will be true for Assist, false for Sleep Talk
let allies: Pokemon[]; const allies: Pokemon[] = this.includeParty
if (this.includeParty) { ? (user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user)
allies = user.isPlayer() ? globalScene.getPlayerParty().filter(p => p !== user) : globalScene.getEnemyParty().filter(p => p !== user); : [ user ];
} else {
allies = [ user ]; // Assist & Sleep Talk consider duplicate moves for their selection (hence why we use an array instead of a set)
} const moveset = allies.flatMap(p => p.moveset);
const partyMoveset = allies.map(p => p.moveset).flat(); const eligibleMoves = moveset.filter(m => m.moveId !== MoveId.NONE && !this.invalidMoves.has(m.moveId) && !m.getMove().name.endsWith(" (N)"));
const moves = partyMoveset.filter(m => !this.invalidMoves.has(m!.moveId) && !m!.getMove().name.endsWith(" (N)")); this.selectedMove = eligibleMoves[user.randBattleSeedInt(eligibleMoves.length)]?.moveId ?? MoveId.NONE; // will fail if 0 length array
if (moves.length === 0) { return this.selectedMove;
return false;
} }
this.moveId = moves[user.randBattleSeedInt(moves.length)].moveId; override getCondition(): MoveConditionFunc {
return true; return (user) => this.getMove(user) !== MoveId.NONE;
};
}
}
export class NaturePowerAttr extends OverrideMoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
let moveId;
switch (globalScene.arena.getTerrainType()) {
// this allows terrains to 'override' the biome move
case TerrainType.NONE:
switch (globalScene.arena.biomeType) {
case BiomeId.TOWN:
moveId = MoveId.ROUND;
break;
case BiomeId.METROPOLIS:
moveId = MoveId.TRI_ATTACK;
break;
case BiomeId.SLUM:
moveId = MoveId.SLUDGE_BOMB;
break;
case BiomeId.PLAINS:
moveId = MoveId.SILVER_WIND;
break;
case BiomeId.GRASS:
moveId = MoveId.GRASS_KNOT;
break;
case BiomeId.TALL_GRASS:
moveId = MoveId.POLLEN_PUFF;
break;
case BiomeId.MEADOW:
moveId = MoveId.GIGA_DRAIN;
break;
case BiomeId.FOREST:
moveId = MoveId.BUG_BUZZ;
break;
case BiomeId.JUNGLE:
moveId = MoveId.LEAF_STORM;
break;
case BiomeId.SEA:
moveId = MoveId.HYDRO_PUMP;
break;
case BiomeId.SWAMP:
moveId = MoveId.MUD_BOMB;
break;
case BiomeId.BEACH:
moveId = MoveId.SCALD;
break;
case BiomeId.LAKE:
moveId = MoveId.BUBBLE_BEAM;
break;
case BiomeId.SEABED:
moveId = MoveId.BRINE;
break;
case BiomeId.ISLAND:
moveId = MoveId.LEAF_TORNADO;
break;
case BiomeId.MOUNTAIN:
moveId = MoveId.AIR_SLASH;
break;
case BiomeId.BADLANDS:
moveId = MoveId.EARTH_POWER;
break;
case BiomeId.DESERT:
moveId = MoveId.SCORCHING_SANDS;
break;
case BiomeId.WASTELAND:
moveId = MoveId.DRAGON_PULSE;
break;
case BiomeId.CONSTRUCTION_SITE:
moveId = MoveId.STEEL_BEAM;
break;
case BiomeId.CAVE:
moveId = MoveId.POWER_GEM;
break;
case BiomeId.ICE_CAVE:
moveId = MoveId.ICE_BEAM;
break;
case BiomeId.SNOWY_FOREST:
moveId = MoveId.FROST_BREATH;
break;
case BiomeId.VOLCANO:
moveId = MoveId.LAVA_PLUME;
break;
case BiomeId.GRAVEYARD:
moveId = MoveId.SHADOW_BALL;
break;
case BiomeId.RUINS:
moveId = MoveId.ANCIENT_POWER;
break;
case BiomeId.TEMPLE:
moveId = MoveId.EXTRASENSORY;
break;
case BiomeId.DOJO:
moveId = MoveId.FOCUS_BLAST;
break;
case BiomeId.FAIRY_CAVE:
moveId = MoveId.ALLURING_VOICE;
break;
case BiomeId.ABYSS:
moveId = MoveId.OMINOUS_WIND;
break;
case BiomeId.SPACE:
moveId = MoveId.DRACO_METEOR;
break;
case BiomeId.FACTORY:
moveId = MoveId.FLASH_CANNON;
break;
case BiomeId.LABORATORY:
moveId = MoveId.ZAP_CANNON;
break;
case BiomeId.POWER_PLANT:
moveId = MoveId.CHARGE_BEAM;
break;
case BiomeId.END:
moveId = MoveId.ETERNABEAM;
break;
}
break;
case TerrainType.MISTY:
moveId = MoveId.MOONBLAST;
break;
case TerrainType.ELECTRIC:
moveId = MoveId.THUNDERBOLT;
break;
case TerrainType.GRASSY:
moveId = MoveId.ENERGY_BALL;
break;
case TerrainType.PSYCHIC:
moveId = MoveId.PSYCHIC;
break;
default:
// Just in case there's no match
moveId = MoveId.TRI_ATTACK;
break;
}
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true });
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true);
return true;
}
}
/**
* Attribute used to copy a previously-used move.
* Used for {@linkcode MoveId.COPYCAT} and {@linkcode MoveId.MIRROR_MOVE}
* @see {@linkcode apply} for move selection and move call
* @extends CallMoveAttr to call a selected move
*/
export class CopyMoveAttr extends CallMoveAttr {
private mirrorMove: boolean;
constructor(mirrorMove: boolean, invalidMoves: ReadonlySet<MoveId> = new Set()) {
super();
this.mirrorMove = mirrorMove;
this.invalidMoves = invalidMoves;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
this.hasTarget = this.mirrorMove;
const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove;
return super.apply(user, target, allMoves[lastMove], args);
}
getCondition(): MoveConditionFunc {
return (user, target, move) => {
if (this.mirrorMove) {
const lastMove = target.getLastXMoves()[0]?.move;
return !!lastMove && !this.invalidMoves.has(lastMove);
} else {
const lastMove = globalScene.currentBattle.lastMove;
return lastMove !== undefined && !this.invalidMoves.has(lastMove);
}
};
} }
} }
@ -8733,7 +8712,7 @@ export function initMoves() {
new SelfStatusMove(MoveId.METRONOME, PokemonType.NORMAL, -1, 10, -1, 0, 1) new SelfStatusMove(MoveId.METRONOME, PokemonType.NORMAL, -1, 10, -1, 0, 1)
.attr(RandomMoveAttr, invalidMetronomeMoves), .attr(RandomMoveAttr, invalidMetronomeMoves),
new StatusMove(MoveId.MIRROR_MOVE, PokemonType.FLYING, -1, 20, -1, 0, 1) new StatusMove(MoveId.MIRROR_MOVE, PokemonType.FLYING, -1, 20, -1, 0, 1)
.attr(CopyMoveAttr, true, invalidMirrorMoveMoves), .attr(CopyMoveAttr, invalidMirrorMoveMoves, true),
new AttackMove(MoveId.SELF_DESTRUCT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1) new AttackMove(MoveId.SELF_DESTRUCT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1)
.attr(SacrificialAttr) .attr(SacrificialAttr)
.makesContact(false) .makesContact(false)
@ -9602,7 +9581,7 @@ export function initMoves() {
.target(MoveTarget.NEAR_ENEMY) .target(MoveTarget.NEAR_ENEMY)
.unimplemented(), .unimplemented(),
new SelfStatusMove(MoveId.COPYCAT, PokemonType.NORMAL, -1, 20, -1, 0, 4) new SelfStatusMove(MoveId.COPYCAT, PokemonType.NORMAL, -1, 20, -1, 0, 4)
.attr(CopyMoveAttr, false, invalidCopycatMoves), .attr(CopyMoveAttr, invalidCopycatMoves),
new StatusMove(MoveId.POWER_SWAP, PokemonType.PSYCHIC, -1, 10, 100, 0, 4) new StatusMove(MoveId.POWER_SWAP, PokemonType.PSYCHIC, -1, 10, 100, 0, 4)
.attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ]) .attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ])
.ignoresSubstitute(), .ignoresSubstitute(),

View File

@ -1,13 +1,12 @@
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { Stat } from "#app/enums/stat"; import { Stat } from "#app/enums/stat";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";
import { CommandPhase } from "#app/phases/command-phase";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Assist", () => { describe("Moves - Assist", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -25,11 +24,12 @@ describe("Moves - Assist", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
// Manual moveset overrides are required for the player pokemon in these tests // Manual moveset overrides are required for the player pokemon in these tests
// because the normal moveset override doesn't allow for accurate testing of moveset changes // because the normal moveset override doesn't allow for accurate testing of moveset changes
game.override game.override
.ability(AbilityId.BALL_FETCH) .ability(AbilityId.BALL_FETCH)
.battleStyle("double") .battleStyle("single")
.disableCrits() .disableCrits()
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.MAGIKARP)
.enemyLevel(100) .enemyLevel(100)
@ -37,69 +37,73 @@ describe("Moves - Assist", () => {
.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH);
}); });
it("should only use an ally's moves", async () => { it("should call a random eligible move from an ally's moveset and apply secondary effects", async () => {
game.override.enemyMoveset(MoveId.SWORDS_DANCE); game.override.battleStyle("double");
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.SHUCKLE]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.SHUCKLE]);
const [feebas, shuckle] = game.scene.getPlayerField(); const [feebas, shuckle] = game.scene.getPlayerField();
// These are all moves Assist cannot call; Sketch will be used to test that it can call other moves properly game.move.changeMoveset(feebas, [MoveId.CIRCLE_THROW, MoveId.ASSIST, MoveId.WOOD_HAMMER, MoveId.ACID_SPRAY]);
game.move.changeMoveset(feebas, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]); game.move.changeMoveset(shuckle, [MoveId.COPYCAT, MoveId.ASSIST, MoveId.TORCH_SONG, MoveId.TACKLE]);
game.move.changeMoveset(shuckle, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]);
game.move.select(MoveId.ASSIST, 0); // Force rolling the first eligible move for both mons (ensuring the user's own moves don't count)
game.move.select(MoveId.SKETCH, 1); vi.spyOn(feebas, "randBattleSeedInt").mockImplementation(() => 0);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER]); vi.spyOn(shuckle, "randBattleSeedInt").mockImplementation(() => 0);
// 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 game.move.select(MoveId.ASSIST, BattlerIndex.PLAYER);
game.move.select(MoveId.ASSIST, BattlerIndex.PLAYER_2);
await game.toEndOfTurn();
expect(feebas.getLastXMoves()[0].move).toBe(MoveId.TORCH_SONG);
expect(shuckle.getLastXMoves()[0].move).toBe(MoveId.WOOD_HAMMER);
expect(feebas.getStatStage(Stat.SPATK)).toBe(1); // Stat raised from Assist --> Torch Song
expect(shuckle.hp).toBeLessThan(shuckle.getMaxHp()); // recoil dmg taken from Assist --> Wood Hammer
expect(feebas.getLastXMoves().map(tm => tm.result)).toEqual([MoveResult.SUCCESS, MoveResult.SUCCESS]);
expect(shuckle.getLastXMoves().map(tm => tm.result)).toEqual([MoveResult.SUCCESS, MoveResult.SUCCESS]);
}); });
it("should fail if there are no allies", async () => { it("should consider off-field allies", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.SHUCKLE]);
const [feebas, shuckle] = game.scene.getPlayerParty();
game.move.changeMoveset(shuckle, MoveId.HYPER_BEAM);
game.move.use(MoveId.ASSIST);
await game.toEndOfTurn();
expect(feebas.getLastXMoves(-1)).toHaveLength(1);
expect(feebas.getLastXMoves()[0]).toMatchObject({
move: MoveId.HYPER_BEAM,
target: [BattlerIndex.ENEMY],
virtual: true,
result: MoveResult.SUCCESS,
});
});
it("should fail if there are no allies, even if user has eligible moves", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const feebas = game.scene.getPlayerPokemon()!; const feebas = game.field.getPlayerPokemon();
game.move.changeMoveset(feebas, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]); game.move.changeMoveset(feebas, [MoveId.ASSIST, MoveId.TACKLE]);
game.move.select(MoveId.ASSIST, 0); game.move.select(MoveId.ASSIST);
await game.toNextTurn(); await game.toEndOfTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
expect(feebas.getLastXMoves(-1)).toHaveLength(1);
expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
}); });
it("should fail if ally has no usable moves and user has usable moves", async () => { it("should fail if allies have no eligible moves", async () => {
game.override.enemyMoveset(MoveId.SWORDS_DANCE);
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.SHUCKLE]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.SHUCKLE]);
const [feebas, shuckle] = game.scene.getPlayerField(); const [feebas, shuckle] = game.scene.getPlayerParty();
game.move.changeMoveset(feebas, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]); // All of these are ineligible moves
game.move.changeMoveset(shuckle, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]); game.move.changeMoveset(shuckle, [MoveId.METRONOME, MoveId.DIG, MoveId.FLY, MoveId.INSTRUCT]);
game.move.select(MoveId.SKETCH, 0); game.move.use(MoveId.ASSIST);
game.move.select(MoveId.PROTECT, 1); await game.toEndOfTurn();
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]);
// Player uses Sketch to copy Swords Dance, Player_2 stalls a turn. Player will attempt Assist and should have no usable moves
await game.toNextTurn();
game.move.select(MoveId.ASSIST, 0);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(MoveId.PROTECT, 1);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(feebas.getLastXMoves(-1)).toHaveLength(1);
}); expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
it("should apply secondary effects of a move", async () => {
game.override.moveset([MoveId.ASSIST, MoveId.WOOD_HAMMER, MoveId.WOOD_HAMMER, MoveId.WOOD_HAMMER]);
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.SHUCKLE]);
const [feebas, shuckle] = game.scene.getPlayerField();
game.move.changeMoveset(feebas, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]);
game.move.changeMoveset(shuckle, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]);
game.move.select(MoveId.ASSIST, 0);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(MoveId.ASSIST, 1);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // should receive recoil damage from Wood Hammer
}); });
}); });

View File

@ -69,7 +69,7 @@ describe("Moves - Copycat", () => {
it("should copy the called move when the last move successfully calls another", async () => { it("should copy the called move when the last move successfully calls another", async () => {
game.override.moveset([MoveId.SPLASH, MoveId.METRONOME]).enemyMoveset(MoveId.COPYCAT); game.override.moveset([MoveId.SPLASH, MoveId.METRONOME]).enemyMoveset(MoveId.COPYCAT);
await game.classicMode.startBattle(); await game.classicMode.startBattle();
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.SWORDS_DANCE); vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.SWORDS_DANCE);
game.move.select(MoveId.METRONOME); game.move.select(MoveId.METRONOME);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); // Player moves first, so enemy can copy Swords Dance await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); // Player moves first, so enemy can copy Swords Dance

View File

@ -1,9 +1,9 @@
import { BattlerIndex } from "#enums/battler-index";
import { RechargingTag, SemiInvulnerableTag } from "#app/data/battler-tags"; import { RechargingTag, SemiInvulnerableTag } from "#app/data/battler-tags";
import type { RandomMoveAttr } from "#app/data/moves/move";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { Stat } from "#app/enums/stat"; import { Stat } from "#app/enums/stat";
import { CommandPhase } from "#app/phases/command-phase"; import { MoveResult } from "#enums/move-result";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
@ -14,7 +14,7 @@ describe("Moves - Metronome", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
let randomMoveAttr: RandomMoveAttr; const randomMoveAttr = allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0];
beforeAll(() => { beforeAll(() => {
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
@ -27,67 +27,103 @@ describe("Moves - Metronome", () => {
}); });
beforeEach(() => { beforeEach(() => {
randomMoveAttr = allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0];
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.moveset([MoveId.METRONOME, MoveId.SPLASH]) .moveset([MoveId.METRONOME, MoveId.SPLASH])
.battleStyle("single") .battleStyle("single")
.startingLevel(100)
.starterSpecies(SpeciesId.REGIELEKI)
.enemyLevel(100)
.enemySpecies(SpeciesId.SHUCKLE) .enemySpecies(SpeciesId.SHUCKLE)
.enemyMoveset(MoveId.SPLASH) .enemyMoveset(MoveId.SPLASH)
.enemyAbility(AbilityId.BALL_FETCH); .enemyAbility(AbilityId.STURDY);
}); });
it("should have one semi-invulnerable turn and deal damage on the second turn when a semi-invulnerable move is called", async () => { it("should not be able to copy MoveId.NONE", async () => {
await game.classicMode.startBattle(); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
// Pick the first move available to use
const player = game.field.getPlayerPokemon();
vi.spyOn(player, "randBattleSeedInt").mockReturnValue(0);
game.move.select(MoveId.METRONOME);
await game.toNextTurn();
const lastMoveStr = MoveId[player.getLastXMoves()[0].move];
expect(lastMoveStr).not.toBe(MoveId[MoveId.NONE]);
expect(lastMoveStr).toBe(MoveId[1]);
});
it("should become semi-invulnerable when using phasing moves", async () => {
vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.DIVE);
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const player = game.scene.getPlayerPokemon()!;
expect(player.getTag(SemiInvulnerableTag)).toBeUndefined();
expect(player.visible).toBe(true);
game.move.select(MoveId.METRONOME);
await game.toNextTurn();
expect(player.getTag(SemiInvulnerableTag)).toBeDefined();
expect(player.visible).toBe(false);
await game.phaseInterceptor.to("TurnEndPhase");
expect(player.getTag(SemiInvulnerableTag)).toBeUndefined();
expect(player.visible).toBe(true);
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
});
it("should apply secondary effects of the called move", async () => {
vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.WOOD_HAMMER);
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
game.move.select(MoveId.METRONOME);
await game.toNextTurn();
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.DIVE);
game.move.select(MoveId.METRONOME); expect(player.hp).toBeLessThan(player.getMaxHp());
await game.toNextTurn(); expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
expect(player.getTag(SemiInvulnerableTag)).toBeTruthy();
await game.toNextTurn();
expect(player.getTag(SemiInvulnerableTag)).toBeFalsy();
expect(enemy.isFullHp()).toBeFalsy();
}); });
it("should apply secondary effects of a move", async () => { it("should count as last move used for Copycat/Mirror Move", async () => {
await game.classicMode.startBattle(); vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.ABSORB);
const player = game.scene.getPlayerPokemon()!; await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.WOOD_HAMMER);
game.move.select(MoveId.METRONOME); game.move.select(MoveId.METRONOME);
await game.move.forceEnemyMove(MoveId.MIRROR_MOVE);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn(); await game.toNextTurn();
expect(player.isFullHp()).toBeFalsy(); const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(player.hp).toBeLessThan(player.getMaxHp());
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
expect(enemy.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
}); });
it("should recharge after using recharge move", async () => { it("should recharge after using recharge moves", async () => {
await game.classicMode.startBattle(); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.HYPER_BEAM); vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.HYPER_BEAM);
vi.spyOn(allMoves[MoveId.HYPER_BEAM], "accuracy", "get").mockReturnValue(100); vi.spyOn(allMoves[MoveId.HYPER_BEAM], "accuracy", "get").mockReturnValue(100);
game.move.select(MoveId.METRONOME); game.move.select(MoveId.METRONOME);
await game.toNextTurn(); await game.toNextTurn();
expect(player.getTag(RechargingTag)).toBeTruthy(); expect(player.getTag(RechargingTag)).toBeDefined();
}); });
it("should only target ally for Aromatic Mist", async () => { it("should only target ally for Aromatic Mist", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double");
await game.classicMode.startBattle([SpeciesId.REGIELEKI, SpeciesId.RATTATA]); await game.classicMode.startBattle([SpeciesId.REGIELEKI, SpeciesId.RATTATA]);
const [leftPlayer, rightPlayer] = game.scene.getPlayerField(); const [leftPlayer, rightPlayer] = game.scene.getPlayerField();
const [leftOpp, rightOpp] = game.scene.getEnemyField(); const [leftOpp, rightOpp] = game.scene.getEnemyField();
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.AROMATIC_MIST); vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.AROMATIC_MIST);
game.move.select(MoveId.METRONOME, 0); game.move.select(MoveId.METRONOME, 0);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(MoveId.SPLASH, 1); game.move.select(MoveId.SPLASH, 1);
await game.toNextTurn(); await game.toNextTurn();
@ -97,9 +133,9 @@ describe("Moves - Metronome", () => {
expect(rightOpp.getStatStage(Stat.SPDEF)).toBe(0); expect(rightOpp.getStatStage(Stat.SPDEF)).toBe(0);
}); });
it("should cause opponent to flee, and not crash for Roar", async () => { it("should cause opponent to flee when using Roar", async () => {
await game.classicMode.startBattle(); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.ROAR); vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.ROAR);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -108,8 +144,9 @@ describe("Moves - Metronome", () => {
const isVisible = enemyPokemon.visible; const isVisible = enemyPokemon.visible;
const hasFled = enemyPokemon.switchOutStatus; const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true); expect(isVisible).toBe(false);
expect(hasFled).toBe(true);
await game.phaseInterceptor.to("CommandPhase"); await game.toNextTurn(); // Check no crash
}); });
}); });

View File

@ -48,7 +48,7 @@ describe("Moves - Moongeist Beam", () => {
// Also covers Photon Geyser and Sunsteel Strike // Also covers Photon Geyser and Sunsteel Strike
it("should not ignore enemy abilities when called by another move, such as metronome", async () => { it("should not ignore enemy abilities when called by another move, such as metronome", async () => {
await game.classicMode.startBattle([SpeciesId.MILOTIC]); await game.classicMode.startBattle([SpeciesId.MILOTIC]);
vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMoveOverride").mockReturnValue( vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMove").mockReturnValue(
MoveId.MOONGEIST_BEAM, MoveId.MOONGEIST_BEAM,
); );

View File

@ -0,0 +1,85 @@
import { allMoves } from "#app/data/data-lists";
import { TerrainType } from "#app/data/terrain";
import { getPokemonNameWithAffix } from "#app/messages";
import { getEnumValues, toReadableString } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id";
import { BiomeId } from "#enums/biome-id";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager";
import i18next from "i18next";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Move - Nature Power", () => {
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
.ability(AbilityId.BALL_FETCH)
.battleStyle("single")
.disableCrits()
.enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.NO_GUARD)
.enemyMoveset(MoveId.SPLASH)
.startingLevel(100)
.enemyLevel(100);
});
const getNaturePowerType = allMoves[MoveId.NATURE_POWER].getAttrs("NaturePowerAttr")[0]["getMoveIdForTerrain"];
it.each(
getEnumValues(BiomeId).map(biome => ({
move: getNaturePowerType(TerrainType.NONE, biome),
moveName: toReadableString(MoveId[getNaturePowerType(TerrainType.NONE, biome)]),
biome,
biomeName: BiomeId[biome],
})),
)("should select $moveName if the current biome is $biomeName", async ({ move, biome }) => {
game.override.startingBiome(biome);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
game.move.use(MoveId.NATURE_POWER);
await game.toEndOfTurn();
const player = game.field.getPlayerPokemon();
expect(player.getLastXMoves(-1).map(m => m.move)).toEqual([move, MoveId.NATURE_POWER]);
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:naturePowerUse", {
pokemonName: getPokemonNameWithAffix(player),
moveName: allMoves[move].name,
}),
);
});
// TODO: Add after terrain override is added
it.todo.each(
getEnumValues(TerrainType).map(terrain => ({
move: getNaturePowerType(terrain, BiomeId.TOWN),
moveName: toReadableString(MoveId[getNaturePowerType(terrain, BiomeId.TOWN)]),
terrain: terrain,
terrainName: TerrainType[terrain],
})),
)("should select $moveName if the current terrain is $terrainName", async ({ move /* terrain */ }) => {
// game.override.terrain(terrainType);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
game.move.use(MoveId.NATURE_POWER);
await game.toEndOfTurn();
const player = game.field.getPlayerPokemon();
expect(player.getLastXMoves(-1).map(m => m.move)).toEqual([move, MoveId.NATURE_POWER]);
});
});

View File

@ -84,7 +84,7 @@ describe("Moves - Sketch", () => {
const randomMoveAttr = allMoves[MoveId.METRONOME].findAttr( const randomMoveAttr = allMoves[MoveId.METRONOME].findAttr(
attr => attr instanceof RandomMoveAttr, attr => attr instanceof RandomMoveAttr,
) as RandomMoveAttr; ) as RandomMoveAttr;
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.FALSE_SWIPE); vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.FALSE_SWIPE);
game.override.enemyMoveset([MoveId.METRONOME]); game.override.enemyMoveset([MoveId.METRONOME]);
await game.classicMode.startBattle([SpeciesId.REGIELEKI]); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);