mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-09 00:49:27 +02:00
Merge e7c37e7135
into 12acaa9590
This commit is contained in:
commit
c6c4c44d80
@ -5,7 +5,7 @@ import { BattleSpec } from "#enums/battle-spec";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import type { Command } from "#enums/command";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
|
||||
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import type { PokeballType } from "#enums/pokeball";
|
||||
@ -70,7 +70,11 @@ export class Battle {
|
||||
public battleScore = 0;
|
||||
public postBattleLoot: PokemonHeldItemModifier[] = [];
|
||||
public escapeAttempts = 0;
|
||||
public lastMove: MoveId;
|
||||
/**
|
||||
* A tracker of the last {@linkcode MoveId} successfully used this battle.
|
||||
*
|
||||
*/
|
||||
public lastMove: MoveId = MoveId.NONE;
|
||||
public battleSeed: string = randomString(16, true);
|
||||
private battleSeedState: string | null = null;
|
||||
public moneyScattered = 0;
|
||||
|
@ -1228,6 +1228,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
constructor(sourceId: number) {
|
||||
super(
|
||||
BattlerTagType.ENCORE,
|
||||
// TODO: This should trigger on turn end
|
||||
[BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE],
|
||||
3,
|
||||
MoveId.ENCORE,
|
||||
|
@ -3130,7 +3130,6 @@ export class OverrideMoveEffectAttr extends MoveAttr {
|
||||
/**
|
||||
* 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.
|
||||
* @extends OverrideMoveEffectAttr
|
||||
* @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 chargeText The text to display when the move is used
|
||||
@ -3175,7 +3174,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
|
||||
* subsequent move this turn. Used for Grass Pledge, Water Pledge, and Fire Pledge.
|
||||
* @extends OverrideMoveEffectAttr
|
||||
*/
|
||||
export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
constructor() {
|
||||
@ -6787,297 +6785,287 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used to call a move.
|
||||
* Used by other move attributes: {@linkcode RandomMoveAttr}, {@linkcode RandomMovesetMoveAttr}, {@linkcode CopyMoveAttr}
|
||||
* @see {@linkcode apply} for move call
|
||||
* @extends OverrideMoveEffectAttr
|
||||
* Abstract attribute used for all move-calling moves, containing common functionality
|
||||
* for executing called moves.
|
||||
*/
|
||||
class CallMoveAttr extends OverrideMoveEffectAttr {
|
||||
protected invalidMoves: ReadonlySet<MoveId>;
|
||||
protected hasTarget: boolean;
|
||||
export abstract class CallMoveAttr extends OverrideMoveEffectAttr {
|
||||
constructor(
|
||||
/**
|
||||
* Whether this move should target the user; default `true`.
|
||||
* If `true`, will unleash non-spread moves against a random eligible target,
|
||||
* as opposed to the move's selected target.
|
||||
*/
|
||||
override selfTarget = true,
|
||||
) {
|
||||
super(selfTarget)
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
// Get eligible targets for move, failing if we can't target anything
|
||||
const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined;
|
||||
const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget);
|
||||
if (moveTargets.targets.length === 0) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:attackFailed"));
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 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 {@linkcode 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);
|
||||
|
||||
// Spread moves and ones with only 1 valid target will use their normal targeting.
|
||||
// If not, target the Mirror Move recipient or else a random enemy in our target list
|
||||
const targets = moveTargets.multiple || moveTargets.targets.length === 1
|
||||
? moveTargets.targets
|
||||
: [this.hasTarget
|
||||
: [this.selfTarget
|
||||
? target.getBattlerIndex()
|
||||
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
|
||||
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", copiedMove.id);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(copiedMove.id), MoveUseMode.FOLLOW_UP);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used to call a random move.
|
||||
* Used for {@linkcode MoveId.METRONOME}
|
||||
* @see {@linkcode apply} for move selection and move call
|
||||
* @extends CallMoveAttr to call a selected move
|
||||
*/
|
||||
export class RandomMoveAttr extends CallMoveAttr {
|
||||
constructor(invalidMoves: ReadonlySet<MoveId>) {
|
||||
super();
|
||||
this.invalidMoves = invalidMoves;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function exists solely to allow tests to override the randomly selected move by mocking this function.
|
||||
*/
|
||||
public getMoveOverride(): MoveId | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* User calls a random moveId.
|
||||
*
|
||||
* Invalid moves are indicated by what is passed in to invalidMoves: {@linkcode invalidMetronomeMoves}
|
||||
* @param user Pokemon that used the move and will call a random move
|
||||
* @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 {
|
||||
const moveIds = getEnumValues(MoveId).map(m => !this.invalidMoves.has(m) && !allMoves[m].name.endsWith(" (N)") ? m : MoveId.NONE);
|
||||
let moveId: MoveId = MoveId.NONE;
|
||||
do {
|
||||
moveId = this.getMoveOverride() ?? moveIds[user.randBattleSeedInt(moveIds.length)];
|
||||
}
|
||||
while (moveId === MoveId.NONE);
|
||||
return super.apply(user, target, allMoves[moveId], args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used to call a random move in the user or party's moveset.
|
||||
* Used for {@linkcode MoveId.ASSIST} and {@linkcode MoveId.SLEEP_TALK}
|
||||
*
|
||||
* 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
|
||||
* Attribute to call a different move based on the current terrain and biome.
|
||||
* Used by {@linkcode MoveId.NATURE_POWER}.
|
||||
*/
|
||||
export class RandomMovesetMoveAttr extends CallMoveAttr {
|
||||
private includeParty: boolean;
|
||||
private moveId: number;
|
||||
constructor(invalidMoves: ReadonlySet<MoveId>, includeParty: boolean = false) {
|
||||
super();
|
||||
this.includeParty = includeParty;
|
||||
this.invalidMoves = invalidMoves;
|
||||
export class NaturePowerAttr extends CallMoveAttr {
|
||||
constructor() {
|
||||
super(false)
|
||||
}
|
||||
|
||||
override getMove(user: Pokemon): MoveId {
|
||||
const moveId = this.getMoveIdForTerrain(globalScene.arena.getTerrainType(), globalScene.arena.biomeType)
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:naturePowerUse", {
|
||||
pokemonName: getPokemonNameWithAffix(user),
|
||||
moveName: allMoves[moveId].name,
|
||||
}))
|
||||
|
||||
return moveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* User calls a random moveId selected in {@linkcode getCondition}
|
||||
* @param user Pokemon that used the move and will call a random move
|
||||
* @param target Pokemon that will be targeted by the random move (if single target)
|
||||
* @param move Move being used
|
||||
* @param args Unused
|
||||
* Helper function to retrieve the correct move for the current terrain and biome.
|
||||
* Made into a separate function for brevity.
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
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() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user);
|
||||
} else {
|
||||
allies = [ user ];
|
||||
}
|
||||
const partyMoveset = allies.flatMap(p => p.moveset);
|
||||
const moves = partyMoveset.filter(m => !this.invalidMoves.has(m.moveId) && !m.getMove().name.endsWith(" (N)"));
|
||||
if (moves.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.moveId = moves[user.randBattleSeedInt(moves.length)].moveId;
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: extend CallMoveAttr
|
||||
export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
let moveId = MoveId.NONE;
|
||||
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;
|
||||
private getMoveIdForTerrain(terrain: TerrainType, biome: BiomeId): MoveId {
|
||||
switch (terrain) {
|
||||
case TerrainType.ELECTRIC:
|
||||
moveId = MoveId.THUNDERBOLT;
|
||||
break;
|
||||
return MoveId.THUNDERBOLT;
|
||||
case TerrainType.GRASSY:
|
||||
moveId = MoveId.ENERGY_BALL;
|
||||
break;
|
||||
return MoveId.ENERGY_BALL;
|
||||
case TerrainType.PSYCHIC:
|
||||
moveId = MoveId.PSYCHIC;
|
||||
break;
|
||||
default:
|
||||
// Just in case there's no match
|
||||
moveId = MoveId.TRI_ATTACK;
|
||||
break;
|
||||
}
|
||||
return MoveId.PSYCHIC;
|
||||
case TerrainType.MISTY:
|
||||
return MoveId.MOONBLAST;
|
||||
}
|
||||
|
||||
// Load the move's animation if we didn't already and unshift a new usage phase
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
|
||||
return true;
|
||||
// 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
|
||||
biome satisfies never;
|
||||
console.warn(`NaturePowerAttr lacks defined move to use for current biome ${toTitleCase(BiomeId[biome])}; 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}
|
||||
* @see {@linkcode apply} for move selection and move call
|
||||
* @extends CallMoveAttr to call a selected move
|
||||
* Abstract class to encompass move-copying-moves with a banlist of invalid moves.
|
||||
*/
|
||||
export class CopyMoveAttr extends CallMoveAttr {
|
||||
private mirrorMove: boolean;
|
||||
constructor(mirrorMove: boolean, invalidMoves: ReadonlySet<MoveId> = new Set()) {
|
||||
super();
|
||||
this.mirrorMove = mirrorMove;
|
||||
abstract class CallMoveAttrWithBanlist extends CallMoveAttr {
|
||||
/**
|
||||
* A {@linkcode ReadonlySet} containing all moves that this {@linkcode MoveAttr} cannot copy,
|
||||
* in addition to unimplemented moves and {@linkcode MoveId.NONE}.
|
||||
* The move should fail if the chosen move is inside this banlist.
|
||||
*/
|
||||
protected readonly invalidMoves: ReadonlySet<MoveId>
|
||||
|
||||
constructor(
|
||||
invalidMoves: ReadonlySet<MoveId>,
|
||||
selfTarget = true,
|
||||
) {
|
||||
super(selfTarget);
|
||||
this.invalidMoves = invalidMoves;
|
||||
}
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, _move: Move, args: any[]): boolean {
|
||||
this.hasTarget = this.mirrorMove;
|
||||
// bang is correct as condition func returns `false` and fails move if no last move exists
|
||||
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)!.move : globalScene.currentBattle.lastMove;
|
||||
return super.apply(user, target, allMoves[lastMove], args);
|
||||
/**
|
||||
* Attribute used to copy the last move executed, either globally or by the specific target.
|
||||
* Used for {@linkcode MoveId.COPYCAT} and {@linkcode MoveId.MIRROR_MOVE}.
|
||||
*/
|
||||
export class CopyMoveAttr extends CallMoveAttrWithBanlist {
|
||||
override getMove(_user: Pokemon, target: Pokemon): MoveId {
|
||||
// If `selfTarget` is `true`, return the last successful move used by anyone on-field.
|
||||
// Otherwise, select the last move used by the target specifically.
|
||||
return this.selfTarget
|
||||
? globalScene.currentBattle.lastMove
|
||||
: target.getLastXMoves()[0]?.move ?? MoveId.NONE
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (_user, target, _move) => {
|
||||
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)?.move : globalScene.currentBattle.lastMove;
|
||||
return !isNullOrUndefined(lastMove) && !this.invalidMoves.has(lastMove);
|
||||
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 CallMoveAttrWithBanlist {
|
||||
constructor(invalidMoves: ReadonlySet<MoveId>) {
|
||||
super(invalidMoves, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @returns The {@linkcode MoveId} that will be called.
|
||||
*/
|
||||
public override getMove(user: Pokemon): MoveId {
|
||||
const moveIds = getEnumValues(MoveId).filter(m => m !== MoveId.NONE && !this.invalidMoves.has(m) && !allMoves[m].name.endsWith(" (N)"));
|
||||
return moveIds[user.randBattleSeedInt(moveIds.length)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used to call a random move in the user or its allies' moveset.
|
||||
* Used for {@linkcode MoveId.ASSIST} and {@linkcode MoveId.SLEEP_TALK}.
|
||||
*
|
||||
* Fails if the user has no callable moves.
|
||||
*/
|
||||
export class RandomMovesetMoveAttr extends RandomMoveAttr {
|
||||
/**
|
||||
* The previously-selected {@linkcode MoveId} for this attribute, or `MoveId.NONE` if none could be found.
|
||||
* Reset to {@linkcode MoveId.NONE} after a successful use.
|
||||
* @defaultValue `MoveId.NONE`
|
||||
*/
|
||||
private selectedMove: MoveId = MoveId.NONE
|
||||
/**
|
||||
* Whether to consider moves from the user's other party members (`true`)
|
||||
* or the user's own moveset (`false`).
|
||||
* @defaultValue `false`.
|
||||
*/
|
||||
private includeParty = false;
|
||||
constructor(invalidMoves: ReadonlySet<MoveId>, includeParty = false) {
|
||||
super(invalidMoves);
|
||||
this.includeParty = includeParty
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a random move from either the user's or its party members' movesets,
|
||||
* or return an already-selected one if one exists.
|
||||
*
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @returns The {@linkcode MoveId} that will be called.
|
||||
*/
|
||||
override getMove(user: Pokemon): MoveId {
|
||||
// If we already have a selected move from the condition function,
|
||||
// re-use and reset it rather than generating another random move
|
||||
if (this.selectedMove) {
|
||||
const m = this.selectedMove;
|
||||
this.selectedMove = MoveId.NONE;
|
||||
return m;
|
||||
}
|
||||
|
||||
// `includeParty` will be true for Assist, false for Sleep Talk
|
||||
const allies: Pokemon[] = this.includeParty
|
||||
? (user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user)
|
||||
: [ 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 eligibleMoves = moveset.filter(m => m.moveId !== MoveId.NONE && !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
|
||||
return this.selectedMove;
|
||||
}
|
||||
|
||||
override getCondition(): MoveConditionFunc {
|
||||
return (user) => this.getMove(user) !== MoveId.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used for moves that cause the target to repeat their last used move.
|
||||
*
|
||||
@ -8775,7 +8763,7 @@ export function initMoves() {
|
||||
new SelfStatusMove(MoveId.METRONOME, PokemonType.NORMAL, -1, 10, -1, 0, 1)
|
||||
.attr(RandomMoveAttr, invalidMetronomeMoves),
|
||||
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)
|
||||
.attr(SacrificialAttr)
|
||||
.makesContact(false)
|
||||
@ -9648,7 +9636,7 @@ export function initMoves() {
|
||||
.target(MoveTarget.NEAR_ENEMY)
|
||||
.unimplemented(),
|
||||
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)
|
||||
.attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ])
|
||||
.ignoresSubstitute(),
|
||||
|
@ -2,20 +2,32 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||
|
||||
export class MoveEndPhase extends PokemonPhase {
|
||||
public readonly phaseName = "MoveEndPhase";
|
||||
/**
|
||||
* Whether the current move was a follow-up attack or not.
|
||||
* Used to prevent ticking down Encore and similar effects when copying moves.
|
||||
*/
|
||||
private wasFollowUp: boolean;
|
||||
/**
|
||||
* Whether the current move successfully executed and showed usage text.
|
||||
* Used to update the "last move used" tracker after successful move usage.
|
||||
*/
|
||||
private passedPreUsageChecks: boolean;
|
||||
|
||||
/** Targets from the preceding MovePhase */
|
||||
private targets: Pokemon[];
|
||||
constructor(battlerIndex: BattlerIndex, targets: Pokemon[], wasFollowUp = false) {
|
||||
|
||||
constructor(battlerIndex: BattlerIndex, targets: Pokemon[], wasFollowUp: boolean, passedPreUsageChecks: boolean) {
|
||||
super(battlerIndex);
|
||||
|
||||
this.targets = targets;
|
||||
this.wasFollowUp = wasFollowUp;
|
||||
this.passedPreUsageChecks = passedPreUsageChecks;
|
||||
}
|
||||
|
||||
start() {
|
||||
@ -26,6 +38,12 @@ export class MoveEndPhase extends PokemonPhase {
|
||||
pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
}
|
||||
|
||||
// Update the "last move used" counter for Copycat and co.
|
||||
if (this.passedPreUsageChecks) {
|
||||
// TODO: Make this check a move in flight instead of a hackjob
|
||||
globalScene.currentBattle.lastMove = pokemon.getLastXMoves()[0]?.move ?? MoveId.NONE;
|
||||
}
|
||||
|
||||
// Remove effects which were set on a Pokemon which removes them on summon (i.e. via Mold Breaker)
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
for (const target of this.targets) {
|
||||
|
@ -42,6 +42,12 @@ export class MovePhase extends BattlePhase {
|
||||
protected failed = false;
|
||||
/** Whether the current move should fail and retain PP. */
|
||||
protected cancelled = false;
|
||||
/**
|
||||
* Whether the move was interrupted prior to showing usage text.
|
||||
* Used to set the "last move" pointer after move end.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
private wasPreInterrupted = true;
|
||||
|
||||
public get pokemon(): Pokemon {
|
||||
return this._pokemon;
|
||||
@ -294,6 +300,10 @@ export class MovePhase extends BattlePhase {
|
||||
const moveQueue = this.pokemon.getMoveQueue();
|
||||
const move = this.move.getMove();
|
||||
|
||||
// Set flag to update Copycat's "last move" counter
|
||||
// TODO: Verify interaction with a failed Focus Punch
|
||||
this.wasPreInterrupted = false;
|
||||
|
||||
// form changes happen even before we know that the move wll execute.
|
||||
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
||||
|
||||
@ -367,11 +377,6 @@ export class MovePhase extends BattlePhase {
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
// Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer.
|
||||
if (!move.hasAttr("CopyMoveAttr") && !isReflected(this.useMode)) {
|
||||
globalScene.currentBattle.lastMove = move.id;
|
||||
}
|
||||
|
||||
// Trigger ability-based user type changes, display move text and then execute move effects.
|
||||
// TODO: Investigate whether PokemonTypeChangeAbAttr can drop the "opponent" parameter
|
||||
applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon: this.pokemon, move, opponent: targets[0] });
|
||||
@ -459,6 +464,9 @@ export class MovePhase extends BattlePhase {
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
// Set flag to update Copycat's "last move" counter
|
||||
this.wasPreInterrupted = false;
|
||||
|
||||
if (!move.applyConditions(this.pokemon, targets[0], move)) {
|
||||
this.failMove(true);
|
||||
return;
|
||||
@ -490,6 +498,7 @@ export class MovePhase extends BattlePhase {
|
||||
this.pokemon.getBattlerIndex(),
|
||||
this.getActiveTargetPokemon(),
|
||||
isVirtual(this.useMode),
|
||||
this.wasPreInterrupted,
|
||||
);
|
||||
|
||||
super.end();
|
||||
|
@ -5,10 +5,9 @@ import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { RandomMoveAttr } from "#moves/move";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Gorilla Tactics", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -83,7 +82,7 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
});
|
||||
|
||||
it("should lock into calling moves, even if also in moveset", async () => {
|
||||
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.TACKLE);
|
||||
game.move.forceMetronomeMove(MoveId.TACKLE);
|
||||
await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]);
|
||||
|
||||
const darmanitan = game.scene.getPlayerPokemon()!;
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { TerrainType } from "#data/terrain";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { RandomMoveAttr } from "#moves/move";
|
||||
import { MoveEndPhase } from "#phases/move-end-phase";
|
||||
import { TurnEndPhase } from "#phases/turn-end-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
// See also: TypeImmunityAbAttr
|
||||
describe("Abilities - Sap Sipper", () => {
|
||||
@ -38,131 +34,98 @@ describe("Abilities - Sap Sipper", () => {
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("raises ATK stat stage by 1 and block effects when activated against a grass attack", async () => {
|
||||
const moveToUse = MoveId.LEAFAGE;
|
||||
|
||||
game.override.moveset(moveToUse);
|
||||
|
||||
it("should nullify all effects of Grass-type attacks and raise ATK by 1 stage", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const initialEnemyHp = enemyPokemon.hp;
|
||||
game.move.use(MoveId.LEAFAGE);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(moveToUse);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
});
|
||||
|
||||
it("raises ATK stat stage by 1 and block effects when activated against a grass status move", async () => {
|
||||
const moveToUse = MoveId.SPORE;
|
||||
|
||||
game.override.moveset(moveToUse);
|
||||
|
||||
it("should work on grass status moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(moveToUse);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.status).toBeUndefined();
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
});
|
||||
|
||||
it("do not activate against status moves that target the field", async () => {
|
||||
const moveToUse = MoveId.GRASSY_TERRAIN;
|
||||
|
||||
game.override.moveset(moveToUse);
|
||||
|
||||
it("should not activate on non Grass-type moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
game.move.select(moveToUse);
|
||||
game.move.use(MoveId.TACKLE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(game.scene.arena.terrain).toBeDefined();
|
||||
expect(game.scene.arena.terrain!.terrainType).toBe(TerrainType.GRASSY);
|
||||
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
it("activate once against multi-hit grass attacks", async () => {
|
||||
const moveToUse = MoveId.BULLET_SEED;
|
||||
|
||||
game.override.moveset(moveToUse);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const initialEnemyHp = enemyPokemon.hp;
|
||||
|
||||
game.move.select(moveToUse);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
});
|
||||
|
||||
it("do not activate against status moves that target the user", async () => {
|
||||
const moveToUse = MoveId.SPIKY_SHIELD;
|
||||
|
||||
game.override.moveset(moveToUse);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(moveToUse);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
|
||||
expect(playerPokemon.getTag(BattlerTagType.SPIKY_SHIELD)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
|
||||
});
|
||||
|
||||
it("activate once against multi-hit grass attacks (metronome)", async () => {
|
||||
const moveToUse = MoveId.METRONOME;
|
||||
|
||||
const randomMoveAttr = allMoves[MoveId.METRONOME].findAttr(
|
||||
attr => attr instanceof RandomMoveAttr,
|
||||
) as RandomMoveAttr;
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.BULLET_SEED);
|
||||
|
||||
game.override.moveset(moveToUse);
|
||||
|
||||
it("should not activate against field-targeted moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const initialEnemyHp = enemyPokemon.hp;
|
||||
game.move.use(MoveId.GRASSY_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(moveToUse);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
expect(game.scene.arena.terrain).toBeDefined();
|
||||
expect(game.scene.arena.terrain!.terrainType).toBe(TerrainType.GRASSY);
|
||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
it("still activates regardless of accuracy check", async () => {
|
||||
game.override.moveset(MoveId.LEAF_BLADE);
|
||||
|
||||
it("should trigger and cancel multi-hit moves, including ones called indirectly", async () => {
|
||||
game.move.forceMetronomeMove(MoveId.BULLET_SEED);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.LEAF_BLADE);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
game.move.use(MoveId.BULLET_SEED);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(1);
|
||||
expect(player.turnData.hitCount).toBe(1);
|
||||
|
||||
game.move.use(MoveId.METRONOME);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(2);
|
||||
expect(player.turnData.hitCount).toBe(1);
|
||||
});
|
||||
|
||||
it("should not activate on self-targeted status moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.use(MoveId.SPIKY_SHIELD);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(player.getTag(BattlerTagType.SPIKY_SHIELD)).toBeDefined();
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(player.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
|
||||
});
|
||||
|
||||
it("should activate even on missed moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
game.move.use(MoveId.LEAF_BLADE);
|
||||
await game.move.forceMiss();
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
@ -23,7 +23,6 @@ describe("Moves - Ability-Ignoring Moves", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.MOONGEIST_BEAM, MoveId.SUNSTEEL_STRIKE, MoveId.PHOTON_GEYSER, MoveId.METRONOME])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.startingLevel(200)
|
||||
.battleStyle("single")
|
||||
@ -43,25 +42,26 @@ describe("Moves - Ability-Ignoring Moves", () => {
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(move);
|
||||
game.move.use(move);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(game.scene.arena.ignoreAbilities).toBe(true);
|
||||
expect(game.scene.arena.ignoringEffectSource).toBe(player.getBattlerIndex());
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.ignoreAbilities).toBe(false);
|
||||
expect(enemy.isFainted()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not ignore enemy abilities when called by Metronome", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MILOTIC]);
|
||||
game.move.forceMetronomeMove(MoveId.PHOTON_GEYSER, true);
|
||||
await game.classicMode.startBattle([SpeciesId.MILOTIC]);
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
game.move.select(MoveId.METRONOME);
|
||||
game.move.use(MoveId.METRONOME);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.isFainted()).toBe(false);
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].move).toBe(MoveId.PHOTON_GEYSER);
|
||||
});
|
||||
|
@ -2,11 +2,12 @@ import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
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", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -24,11 +25,12 @@ describe("Moves - Assist", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
// 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
|
||||
game.override
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("double")
|
||||
.ability(AbilityId.NO_GUARD)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyLevel(100)
|
||||
@ -36,66 +38,72 @@ describe("Moves - Assist", () => {
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should only use an ally's moves", async () => {
|
||||
game.override.enemyMoveset(MoveId.SWORDS_DANCE);
|
||||
it("should call a random eligible move from an ally's moveset and apply secondary effects", async () => {
|
||||
game.override.battleStyle("double");
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.SHUCKLE]);
|
||||
|
||||
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.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]);
|
||||
game.move.changeMoveset(shuckle, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]);
|
||||
game.move.changeMoveset(feebas, [MoveId.CIRCLE_THROW, MoveId.ASSIST, MoveId.WOOD_HAMMER, MoveId.ACID_SPRAY]);
|
||||
game.move.changeMoveset(shuckle, [MoveId.COPYCAT, MoveId.ASSIST, MoveId.TORCH_SONG, MoveId.TACKLE]);
|
||||
|
||||
game.move.select(MoveId.ASSIST, 0);
|
||||
game.move.select(MoveId.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();
|
||||
// Force rolling the first eligible move for both mons
|
||||
vi.spyOn(feebas, "randBattleSeedInt").mockImplementation(() => 0);
|
||||
vi.spyOn(shuckle, "randBattleSeedInt").mockImplementation(() => 0);
|
||||
|
||||
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(-1).map(tm => tm.result)).toEqual([MoveResult.SUCCESS, MoveResult.SUCCESS]);
|
||||
expect(shuckle.getLastXMoves(-1).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(2);
|
||||
expect(feebas.getLastXMoves()[0]).toMatchObject({
|
||||
move: MoveId.HYPER_BEAM,
|
||||
useMode: MoveUseMode.INDIRECT,
|
||||
result: MoveResult.SUCCESS,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail if there are no allies, even if user has eligible moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const feebas = game.scene.getPlayerPokemon()!;
|
||||
game.move.changeMoveset(feebas, [MoveId.ASSIST, MoveId.SKETCH, MoveId.PROTECT, MoveId.DRAGON_TAIL]);
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
game.move.changeMoveset(feebas, [MoveId.ASSIST, MoveId.TACKLE]);
|
||||
|
||||
game.move.select(MoveId.ASSIST, 0);
|
||||
await game.toNextTurn();
|
||||
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
game.move.select(MoveId.ASSIST);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
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 () => {
|
||||
game.override.enemyMoveset(MoveId.SWORDS_DANCE);
|
||||
it("should fail if allies have no eligible moves", async () => {
|
||||
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]);
|
||||
const [feebas, shuckle] = game.scene.getPlayerParty();
|
||||
// All of these are ineligible moves
|
||||
game.move.changeMoveset(shuckle, [MoveId.METRONOME, MoveId.DIG, MoveId.FLY]);
|
||||
|
||||
game.move.select(MoveId.SKETCH, 0);
|
||||
game.move.select(MoveId.PROTECT, 1);
|
||||
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);
|
||||
game.move.select(MoveId.PROTECT, 1);
|
||||
await game.toNextTurn();
|
||||
game.move.use(MoveId.ASSIST);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should apply secondary effects of a move", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.SHUCKLE]);
|
||||
|
||||
const [feebas, shuckle] = game.scene.getPlayerField();
|
||||
game.move.changeMoveset(feebas, [MoveId.ASSIST, MoveId.WOOD_HAMMER]);
|
||||
game.move.changeMoveset(shuckle, [MoveId.ASSIST, MoveId.WOOD_HAMMER]);
|
||||
|
||||
game.move.select(MoveId.ASSIST, 0);
|
||||
game.move.select(MoveId.ASSIST, 1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // should receive recoil damage from Wood Hammer
|
||||
expect(feebas.getLastXMoves(-1)).toHaveLength(1);
|
||||
expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
||||
|
@ -4,11 +4,10 @@ import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { RandomMoveAttr } from "#moves/move";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Chilly Reception", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -116,7 +115,7 @@ describe("Moves - Chilly Reception", () => {
|
||||
});
|
||||
|
||||
it("should succeed without message if called indirectly", async () => {
|
||||
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.CHILLY_RECEPTION);
|
||||
game.move.forceMetronomeMove(MoveId.CHILLY_RECEPTION);
|
||||
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
|
||||
|
||||
const [slowking, meowth] = game.scene.getPlayerParty();
|
||||
|
@ -2,9 +2,9 @@ import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -26,7 +26,6 @@ describe("Moves - Copycat", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.COPYCAT, MoveId.SPIKY_SHIELD, MoveId.SWORDS_DANCE, MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
@ -35,58 +34,81 @@ describe("Moves - Copycat", () => {
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should copy the last move successfully executed", async () => {
|
||||
game.override.enemyMoveset(MoveId.SUCKER_PUNCH);
|
||||
it("should copy the last move successfully executed by any Pokemon", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SWORDS_DANCE);
|
||||
await game.toNextTurn();
|
||||
game.move.use(MoveId.COPYCAT);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
game.move.select(MoveId.COPYCAT); // Last successful move should be Swords Dance
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(4);
|
||||
const player = game.field.getPlayerPokemon();
|
||||
expect(player.getStatStage(Stat.ATK)).toBe(2);
|
||||
expect(player.getLastXMoves()[0].move).toBe(MoveId.SWORDS_DANCE);
|
||||
});
|
||||
|
||||
it("should fail when the last move used is not a valid Copycat move", async () => {
|
||||
game.override.enemyMoveset(MoveId.PROTECT); // Protect is not a valid move for Copycat to copy
|
||||
it('should update "last move" tracker for moves failing conditions, but not pre-move interrupts', async () => {
|
||||
game.override.enemyStatusEffect(StatusEffect.SLEEP);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SPIKY_SHIELD); // Spiky Shield is not a valid move for Copycat to copy
|
||||
await game.toNextTurn();
|
||||
game.move.use(MoveId.SUCKER_PUNCH);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
// Enemy is asleep and should not have updated tracker
|
||||
expect(game.scene.currentBattle.lastMove).toBe(MoveId.NONE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
// Player sucker punch failed conditions, but still updated tracker
|
||||
expect(game.scene.currentBattle.lastMove).toBe(MoveId.SUCKER_PUNCH);
|
||||
|
||||
const player = game.field.getPlayerPokemon();
|
||||
expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail if no prior moves have been made", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.COPYCAT);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail if the last move used is not a valid Copycat move", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.use(MoveId.COPYCAT);
|
||||
await game.move.forceEnemyMove(MoveId.PROTECT);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should copy the called move when the last move successfully calls another", async () => {
|
||||
game.override.moveset([MoveId.SPLASH, MoveId.METRONOME]).enemyMoveset(MoveId.COPYCAT);
|
||||
await game.classicMode.startBattle([SpeciesId.DRAMPA]);
|
||||
game.move.forceMetronomeMove(MoveId.SWORDS_DANCE, true);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.METRONOME);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); // Player moves first so enemy can copy Swords Dance
|
||||
game.move.use(MoveId.METRONOME);
|
||||
game.move.forceMetronomeMove(MoveId.SWORDS_DANCE, true);
|
||||
await game.move.forceEnemyMove(MoveId.COPYCAT);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); // Player moves first, so enemy can copy Swords Dance
|
||||
await game.toNextTurn();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy.getLastXMoves()[0]).toMatchObject({
|
||||
move: MoveId.SWORDS_DANCE,
|
||||
result: MoveResult.SUCCESS,
|
||||
useMode: MoveUseMode.FOLLOW_UP,
|
||||
});
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(2);
|
||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(2);
|
||||
});
|
||||
|
||||
it("should apply move secondary effects", async () => {
|
||||
game.override.enemyMoveset(MoveId.ACID_SPRAY); // Secondary effect lowers SpDef by 2 stages
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.COPYCAT);
|
||||
game.move.use(MoveId.COPYCAT);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2);
|
||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.SPDEF)).toBe(-2);
|
||||
});
|
||||
});
|
||||
|
@ -5,9 +5,8 @@ import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { RandomMoveAttr } from "#moves/move";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Disable", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -129,7 +128,7 @@ describe("Moves - Disable", () => {
|
||||
{ name: "Copycat", moveId: MoveId.COPYCAT },
|
||||
{ name: "Metronome", moveId: MoveId.METRONOME },
|
||||
])("should ignore virtual moves called by $name", async ({ moveId }) => {
|
||||
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.ABSORB);
|
||||
game.move.forceMetronomeMove(MoveId.ABSORB);
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
const playerMon = game.scene.getPlayerPokemon()!;
|
||||
|
@ -7,7 +7,6 @@ import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { RandomMoveAttr } from "#moves/move";
|
||||
import type { MovePhase } from "#phases/move-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
@ -127,25 +126,24 @@ describe("Moves - Instruct", () => {
|
||||
expect(game.scene.getPlayerPokemon()!.turnData.attacksReceived.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should add moves to move queue for copycat", async () => {
|
||||
game.override.battleStyle("double").moveset(MoveId.INSTRUCT).enemyLevel(5);
|
||||
it("should be considered as the last move used for copycat", async () => {
|
||||
game.override.battleStyle("double").enemyLevel(5);
|
||||
await game.classicMode.startBattle([SpeciesId.AMOONGUSS]);
|
||||
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField()!;
|
||||
game.move.changeMoveset(enemy1, MoveId.WATER_GUN);
|
||||
game.move.changeMoveset(enemy2, MoveId.COPYCAT);
|
||||
|
||||
game.move.select(MoveId.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.use(MoveId.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
instructSuccess(enemy1, MoveId.WATER_GUN);
|
||||
// amoonguss gets hit by water gun thrice; once by original attack, once by instructed use and once by copycat
|
||||
expect(game.scene.getPlayerPokemon()!.turnData.attacksReceived.length).toBe(3);
|
||||
expect(enemy2.getLastXMoves()[0].move).toBe(MoveId.WATER_GUN);
|
||||
});
|
||||
|
||||
it("should fail on metronomed moves, even if also in moveset", async () => {
|
||||
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.ABSORB);
|
||||
game.move.forceMetronomeMove(MoveId.ABSORB);
|
||||
await game.classicMode.startBattle([SpeciesId.AMOONGUSS]);
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
@ -8,7 +8,6 @@ import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import type { RandomMoveAttr } from "#moves/move";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@ -17,8 +16,6 @@ describe("Moves - Metronome", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
let randomMoveAttr: RandomMoveAttr;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
@ -30,62 +27,102 @@ describe("Moves - Metronome", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
randomMoveAttr = allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0];
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.METRONOME, MoveId.SPLASH])
|
||||
.battleStyle("single")
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.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([SpeciesId.REGIELEKI]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.DIVE);
|
||||
|
||||
game.move.select(MoveId.METRONOME);
|
||||
// Pick the first move available to use
|
||||
const player = game.field.getPlayerPokemon();
|
||||
vi.spyOn(player, "randBattleSeedInt").mockReturnValue(0);
|
||||
game.move.use(MoveId.METRONOME);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.getTag(SemiInvulnerableTag)).toBeTruthy();
|
||||
|
||||
await game.toNextTurn();
|
||||
expect(player.getTag(SemiInvulnerableTag)).toBeFalsy();
|
||||
expect(enemy.isFullHp()).toBeFalsy();
|
||||
const lastMoveStr = MoveId[player.getLastXMoves()[0].move];
|
||||
expect(lastMoveStr).not.toBe(MoveId[MoveId.NONE]);
|
||||
expect(lastMoveStr).toBe(MoveId[1]);
|
||||
});
|
||||
|
||||
it("should apply secondary effects of a move", async () => {
|
||||
it("should become semi-invulnerable when using phasing moves", async () => {
|
||||
game.move.forceMetronomeMove(MoveId.DIVE);
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.WOOD_HAMMER);
|
||||
|
||||
game.move.select(MoveId.METRONOME);
|
||||
const player = game.field.getPlayerPokemon();
|
||||
expect(player.getTag(SemiInvulnerableTag)).toBeUndefined();
|
||||
expect(player.visible).toBe(true);
|
||||
|
||||
game.move.use(MoveId.METRONOME);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.isFullHp()).toBeFalsy();
|
||||
expect(player.getTag(SemiInvulnerableTag)).toBeDefined();
|
||||
expect(player.visible).toBe(false);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(player.getTag(SemiInvulnerableTag)).toBeUndefined();
|
||||
expect(player.visible).toBe(true);
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
});
|
||||
|
||||
it("should recharge after using recharge move", async () => {
|
||||
it("should apply secondary effects of the called move", async () => {
|
||||
game.move.forceMetronomeMove(MoveId.WOOD_HAMMER);
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.HYPER_BEAM);
|
||||
|
||||
game.move.use(MoveId.METRONOME);
|
||||
await game.toNextTurn();
|
||||
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
expect(player.hp).toBeLessThan(player.getMaxHp());
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
});
|
||||
|
||||
it("should count as last move used for Copycat/Mirror Move", async () => {
|
||||
game.move.forceMetronomeMove(MoveId.ABSORB, true);
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
|
||||
game.move.use(MoveId.METRONOME);
|
||||
await game.move.forceEnemyMove(MoveId.MIRROR_MOVE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.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 moves", async () => {
|
||||
game.move.forceMetronomeMove(MoveId.HYPER_BEAM);
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
|
||||
const player = game.field.getPlayerPokemon();
|
||||
vi.spyOn(allMoves[MoveId.HYPER_BEAM], "accuracy", "get").mockReturnValue(100);
|
||||
|
||||
game.move.select(MoveId.METRONOME);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.getTag(RechargingTag)).toBeTruthy();
|
||||
expect(player.getTag(RechargingTag)).toBeDefined();
|
||||
});
|
||||
|
||||
it("should charge for charging moves while still maintaining follow-up status", async () => {
|
||||
game.override.moveset([]).enemyMoveset(MoveId.SPITE);
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.SOLAR_BEAM);
|
||||
game.move.forceMetronomeMove(MoveId.SOLAR_BEAM);
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
|
||||
// We need to override movesets here
|
||||
const player = game.field.getPlayerPokemon();
|
||||
game.move.changeMoveset(player, [MoveId.METRONOME, MoveId.SOLAR_BEAM]);
|
||||
|
||||
@ -93,19 +130,21 @@ describe("Moves - Metronome", () => {
|
||||
expect(metronomeMove).toBeDefined();
|
||||
expect(solarBeamMove).toBeDefined();
|
||||
|
||||
game.move.select(MoveId.METRONOME);
|
||||
game.move.use(MoveId.METRONOME);
|
||||
await game.move.forceEnemyMove(MoveId.SPITE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(player.getTag(BattlerTagType.CHARGING)).toBeTruthy();
|
||||
// Solar beam charged up, but did not block Spite from reducing Metronome's PP
|
||||
expect(player.getTag(BattlerTagType.CHARGING)).toBeDefined();
|
||||
const turn1PpUsed = metronomeMove.ppUsed;
|
||||
expect.soft(turn1PpUsed).toBeGreaterThan(1);
|
||||
expect(solarBeamMove.ppUsed).toBe(0);
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.getTag(BattlerTagType.CHARGING)).toBeFalsy();
|
||||
expect(player.getTag(BattlerTagType.CHARGING)).toBeUndefined();
|
||||
const turn2PpUsed = metronomeMove.ppUsed - turn1PpUsed;
|
||||
expect(turn2PpUsed).toBeGreaterThan(1);
|
||||
expect(solarBeamMove.ppUsed).toBe(0);
|
||||
@ -119,12 +158,13 @@ describe("Moves - Metronome", () => {
|
||||
it("should only target ally for Aromatic Mist", async () => {
|
||||
game.override.battleStyle("double");
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI, SpeciesId.RATTATA]);
|
||||
|
||||
const [leftPlayer, rightPlayer] = game.scene.getPlayerField();
|
||||
const [leftOpp, rightOpp] = game.scene.getEnemyField();
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.AROMATIC_MIST);
|
||||
game.move.forceMetronomeMove(MoveId.AROMATIC_MIST);
|
||||
|
||||
game.move.select(MoveId.METRONOME, 0);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
game.move.use(MoveId.METRONOME, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(rightPlayer.getStatStage(Stat.SPDEF)).toBe(1);
|
||||
@ -133,18 +173,19 @@ describe("Moves - Metronome", () => {
|
||||
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([SpeciesId.REGIELEKI]);
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.ROAR);
|
||||
game.move.forceMetronomeMove(MoveId.ROAR);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.METRONOME);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
game.move.use(MoveId.METRONOME);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const isVisible = enemyPokemon.visible;
|
||||
const hasFled = enemyPokemon.switchOutStatus;
|
||||
expect(!isVisible && hasFled).toBe(true);
|
||||
expect(isVisible).toBe(false);
|
||||
expect(hasFled).toBe(true);
|
||||
|
||||
await game.toNextTurn();
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
@ -25,7 +26,6 @@ describe("Moves - Mirror Move", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.MIRROR_MOVE, MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
@ -34,49 +34,52 @@ describe("Moves - Mirror Move", () => {
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should use the last move that the target used on the user", async () => {
|
||||
game.override.battleStyle("double").enemyMoveset([MoveId.TACKLE, MoveId.GROWL]);
|
||||
it("should use the last move that the target used against it", async () => {
|
||||
game.override.battleStyle("double");
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.MIRROR_MOVE, 0, BattlerIndex.ENEMY); // target's last move is Tackle, enemy should receive damage from Mirror Move copying Tackle
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2);
|
||||
await game.move.selectEnemyMove(MoveId.GROWL, BattlerIndex.PLAYER_2);
|
||||
game.move.use(MoveId.MIRROR_MOVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.use(MoveId.MIRROR_MOVE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||
await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getEnemyField()[0].isFullHp()).toBeFalsy();
|
||||
// Feebas copied enemy tackle against it;
|
||||
// player 2 copied enemy swords dance and used it on itself
|
||||
const [feebas, magikarp] = game.scene.getPlayerField();
|
||||
expect(feebas.getLastXMoves()[0]).toMatchObject({
|
||||
move: MoveId.TACKLE,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
useMode: MoveUseMode.FOLLOW_UP,
|
||||
});
|
||||
expect(game.field.getEnemyPokemon().isFullHp()).toBe(false);
|
||||
expect(magikarp.getLastXMoves()[0]).toMatchObject({
|
||||
move: MoveId.SWORDS_DANCE,
|
||||
targets: [BattlerIndex.PLAYER_2],
|
||||
useMode: MoveUseMode.FOLLOW_UP,
|
||||
});
|
||||
expect(magikarp.getStatStage(Stat.ATK)).toBe(2);
|
||||
});
|
||||
|
||||
it("should apply secondary effects of a move", async () => {
|
||||
game.override.enemyMoveset(MoveId.ACID_SPRAY);
|
||||
it("should apply secondary effects of the called move", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.MIRROR_MOVE);
|
||||
game.move.use(MoveId.MIRROR_MOVE);
|
||||
await game.move.forceEnemyMove(MoveId.ACID_SPRAY);
|
||||
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(MoveId.GROWL);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.MIRROR_MOVE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
|
||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.SPDEF)).toBe(-2);
|
||||
});
|
||||
|
||||
it("should fail if the target has not used any moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.MIRROR_MOVE);
|
||||
game.move.use(MoveId.MIRROR_MOVE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
||||
|
86
test/moves/nature-power.test.ts
Normal file
86
test/moves/nature-power.test.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { TerrainType } from "#app/data/terrain";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
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/test-utils/game-manager";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
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")
|
||||
.criticalHits(false)
|
||||
.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: toTitleCase(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: toTitleCase(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]);
|
||||
});
|
||||
});
|
@ -1,15 +1,13 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { RandomMoveAttr } from "#moves/move";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Sketch", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -81,20 +79,18 @@ describe("Moves - Sketch", () => {
|
||||
});
|
||||
|
||||
it("should sketch moves that call other moves", async () => {
|
||||
const randomMoveAttr = allMoves[MoveId.METRONOME].findAttr(
|
||||
attr => attr instanceof RandomMoveAttr,
|
||||
) as RandomMoveAttr;
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(MoveId.FALSE_SWIPE);
|
||||
|
||||
game.override.enemyMoveset([MoveId.METRONOME]);
|
||||
game.move.forceMetronomeMove(MoveId.FALSE_SWIPE);
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
playerPokemon.moveset = [new PokemonMove(MoveId.SKETCH)];
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon()!;
|
||||
game.move.changeMoveset(playerPokemon, MoveId.SKETCH);
|
||||
|
||||
// Opponent uses Metronome -> False Swipe, then player uses Sketch, which should sketch Metronome
|
||||
game.move.select(MoveId.SKETCH);
|
||||
await game.move.forceEnemyMove(MoveId.METRONOME);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(playerPokemon.moveset[0]?.moveId).toBe(MoveId.METRONOME);
|
||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); // Make sure opponent actually used False Swipe
|
||||
|
@ -331,7 +331,7 @@ export class MoveHelper extends GameManagerHelper {
|
||||
* @returns The spy that for Metronome that was mocked (Usually unneeded).
|
||||
*/
|
||||
public forceMetronomeMove(move: MoveId, once = false): MockInstance {
|
||||
const spy = vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMoveOverride");
|
||||
const spy = vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMove");
|
||||
if (once) {
|
||||
spy.mockReturnValueOnce(move);
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user