Reverted a couple doc changes

This commit is contained in:
Bertie690 2025-05-10 16:28:22 -04:00
parent c2e7c95620
commit da6443562b
7 changed files with 332 additions and 373 deletions

View File

@ -2335,18 +2335,18 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
// phase list (which could be after CommandPhase for example) // phase list (which could be after CommandPhase for example)
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, this.stats, this.stages)); globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, this.stats, this.stages));
} else { } else {
for (const opponent of pokemon.getOpponents()) { for (const opponent of pokemon.getOpponents()) {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
if (this.intimidate) { if (this.intimidate) {
applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated); applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated);
applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated); applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated);
if (opponent.getTag(BattlerTagType.SUBSTITUTE)) { if (opponent.getTag(BattlerTagType.SUBSTITUTE)) {
cancelled.value = true; cancelled.value = true;
}
} }
} if (!cancelled.value) {
if (!cancelled.value) { globalScene.unshiftPhase(new StatStageChangePhase(opponent.getBattlerIndex(), false, this.stats, this.stages));
globalScene.unshiftPhase(new StatStageChangePhase(opponent.getBattlerIndex(), false, this.stats, this.stages));
} }
} }
} }
@ -4399,12 +4399,12 @@ export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr {
* @extends AbAttr * @extends AbAttr
*/ */
export class PostMoveUsedAbAttr extends AbAttr { export class PostMoveUsedAbAttr extends AbAttr {
canApplyPostMoveUsed( canApplyPostMoveUsed(
pokemon: Pokemon, pokemon: Pokemon,
move: PokemonMove, move: PokemonMove,
source: Pokemon, source: Pokemon,
targets: BattlerIndex[], targets: BattlerIndex[],
simulated: boolean, simulated: boolean,
args: any[]): boolean { args: any[]): boolean {
return true; return true;
} }
@ -4414,7 +4414,7 @@ export class PostMoveUsedAbAttr extends AbAttr {
move: PokemonMove, move: PokemonMove,
source: Pokemon, source: Pokemon,
targets: BattlerIndex[], targets: BattlerIndex[],
simulated: boolean, simulated: boolean,
args: any[], args: any[],
): void {} ): void {}
} }
@ -4899,7 +4899,7 @@ export class BlockRedirectAbAttr extends AbAttr { }
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class ReduceStatusEffectDurationAbAttr extends AbAttr { export class ReduceStatusEffectDurationAbAttr extends AbAttr {
private statusEffect: StatusEffect; private statusEffect: StatusEffect;
constructor(statusEffect: StatusEffect) { constructor(statusEffect: StatusEffect) {
super(false); super(false);
@ -4908,7 +4908,7 @@ private statusEffect: StatusEffect;
} }
override canApply(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { override canApply(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
return args[1] instanceof NumberHolder && args[0] === this.statusEffect; return args[1] instanceof NumberHolder && args[0] === this.statusEffect;
} }
/** /**
@ -5557,7 +5557,7 @@ class ForceSwitchOutHelper {
* - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated. * - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated.
*/ */
if (switchOutTarget instanceof PlayerPokemon) { if (switchOutTarget instanceof PlayerPokemon) {
if (globalScene.getPlayerParty().every(p => !p.isActive(true))) { if (globalScene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false; return false;
} }
@ -5890,7 +5890,7 @@ export function applyPostMoveUsedAbAttrs(
move: PokemonMove, move: PokemonMove,
source: Pokemon, source: Pokemon,
targets: BattlerIndex[], targets: BattlerIndex[],
simulated = false, simulated = false,
...args: any[] ...args: any[]
): void { ): void {
applyAbAttrsInternal<PostMoveUsedAbAttr>( applyAbAttrsInternal<PostMoveUsedAbAttr>(
@ -6901,8 +6901,8 @@ export function initAbilities() {
new Ability(Abilities.ANALYTIC, 5) new Ability(Abilities.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user, target, move) => { .attr(MovePowerBoostAbAttr, (user, target, move) => {
// Boost power if all other Pokemon have already moved (no other moves are slated to execute) // Boost power if all other Pokemon have already moved (no other moves are slated to execute)
const laterMovePhase = globalScene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user?.id); const movePhase = globalScene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user?.id);
return isNullOrUndefined(laterMovePhase); return isNullOrUndefined(movePhase);
}, 1.3), }, 1.3),
new Ability(Abilities.ILLUSION, 5) new Ability(Abilities.ILLUSION, 5)
// The Pokemon generate an illusion if it's available // The Pokemon generate an illusion if it's available

View File

@ -51,10 +51,6 @@ export enum BattlerTagLapseType {
MOVE, MOVE,
PRE_MOVE, PRE_MOVE,
AFTER_MOVE, AFTER_MOVE,
/**
* TODO: Stop treating this like a catch-all "semi invulnerability" tag;
* we may want to use this for other stuff later
*/
MOVE_EFFECT, MOVE_EFFECT,
TURN_END, TURN_END,
HIT, HIT,
@ -65,7 +61,7 @@ export enum BattlerTagLapseType {
export class BattlerTag { export class BattlerTag {
public tagType: BattlerTagType; public tagType: BattlerTagType;
public lapseTypes: BattlerTagLapseType[]; // TODO: Make this a set public lapseTypes: BattlerTagLapseType[];
public turnCount: number; public turnCount: number;
public sourceMove: Moves; public sourceMove: Moves;
public sourceId?: number; public sourceId?: number;
@ -98,7 +94,7 @@ export class BattlerTag {
onOverlap(_pokemon: Pokemon): void {} onOverlap(_pokemon: Pokemon): void {}
/** /**
* Trigger and tick down this {@linkcode BattlerTag}'s duration. * Tick down this {@linkcode BattlerTag}'s duration.
* @returns `true` if the tag should be kept (`turnCount` > 0`) * @returns `true` if the tag should be kept (`turnCount` > 0`)
*/ */
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
@ -186,21 +182,21 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
} }
/** /**
* Check if this tag is currently restricting a move's use. * Gets whether this tag is restricting a move.
* *
* @param move - The {@linkcode Moves | move ID} whose usability is being checked. * @param move - {@linkcode Moves} ID to check restriction for.
* @param user - The {@linkcode Pokemon} using the move. * @param user - The {@linkcode Pokemon} involved
* @returns Whether the given move is restricted by this tag. * @returns `true` if the move is restricted by this tag, otherwise `false`.
*/ */
public abstract isMoveRestricted(move: Moves, user?: Pokemon): boolean; public abstract isMoveRestricted(move: Moves, user?: Pokemon): boolean;
/** /**
* Check if this tag is restricting a move during target selection. * Checks if this tag is restricting a move based on a user's decisions during the target selection phase
* Returns `false` by default unless overridden by a child class. *
* @param _move - The {@linkcode Moves | move ID} whose selectability is being checked * @param {Moves} _move {@linkcode Moves} move ID to check restriction for
* @param _user - The {@linkcode Pokemon} using the move. * @param {Pokemon} _user {@linkcode Pokemon} the user of the above move
* @param _target - The {@linkcode Pokemon} being targeted * @param {Pokemon} _target {@linkcode Pokemon} the target of the above move
* @returns Whether the given move should be unselectable when choosing targets. * @returns {boolean} `false` unless overridden by the child tag
*/ */
isMoveTargetRestricted(_move: Moves, _user: Pokemon, _target: Pokemon): boolean { isMoveTargetRestricted(_move: Moves, _user: Pokemon, _target: Pokemon): boolean {
return false; return false;
@ -346,10 +342,9 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
/** /**
* @override * @override
* Display the text that occurs when a move is interrupted via Disable. * @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move
* @param pokemon - The {@linkcode Pokemon} attempting to use the restricted move * @param {Moves} move {@linkcode Moves} ID of the move being interrupted
* @param move - The {@linkcode Moves | move ID} of the move being interrupted * @returns {string} text to display when the move is interrupted
* @returns The text to display when the given move is interrupted
*/ */
override interruptedText(pokemon: Pokemon, move: Moves): string { override interruptedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:disableInterruptedMove", { return i18next.t("battle:disableInterruptedMove", {
@ -367,6 +362,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
/** /**
* Tag used by Gorilla Tactics to restrict the user to using only one move. * Tag used by Gorilla Tactics to restrict the user to using only one move.
* @extends MoveRestrictionBattlerTag
*/ */
export class GorillaTacticsTag extends MoveRestrictionBattlerTag { export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
private moveId = Moves.NONE; private moveId = Moves.NONE;
@ -416,10 +412,11 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
} }
/** /**
*
* @override * @override
* @param pokemon - The {@linkcode Pokemon} attempting to select a move * @param {Pokemon} pokemon n/a
* @param _move - Unused * @param {Moves} _move {@linkcode Moves} ID of the move being denied
* @returns The text to display when the move is rendered unselectable * @returns {string} text to display when the move is denied
*/ */
override selectionDeniedText(pokemon: Pokemon, _move: Moves): string { override selectionDeniedText(pokemon: Pokemon, _move: Moves): string {
return i18next.t("battle:canOnlyUseMove", { return i18next.t("battle:canOnlyUseMove", {
@ -640,7 +637,7 @@ class NoRetreatTag extends TrappedTag {
*/ */
export class FlinchedTag extends BattlerTag { export class FlinchedTag extends BattlerTag {
constructor(sourceMove: Moves) { constructor(sourceMove: Moves) {
super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1, sourceMove); super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 0, sourceMove);
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -1481,6 +1478,10 @@ export class WrapTag extends DamagingTrapTag {
} }
export abstract class VortexTrapTag extends DamagingTrapTag { export abstract class VortexTrapTag extends DamagingTrapTag {
constructor(tagType: BattlerTagType, commonAnim: CommonAnim, turnCount: number, sourceMove: Moves, sourceId: number) {
super(tagType, commonAnim, turnCount, sourceMove, sourceId);
}
getTrapMessage(pokemon: Pokemon): string { getTrapMessage(pokemon: Pokemon): string {
return i18next.t("battlerTags:vortexOnTrap", { return i18next.t("battlerTags:vortexOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
@ -2722,7 +2723,7 @@ export class ExposedTag extends BattlerTag {
/** /**
* Tag that prevents HP recovery from held items and move effects. It also blocks the usage of recovery moves. * Tag that prevents HP recovery from held items and move effects. It also blocks the usage of recovery moves.
* Applied by moves: {@linkcode Moves.HEAL_BLOCK | Heal Block (5 turns)}, {@linkcode Moves.PSYCHIC_NOISE | Psychic Noise (2 turns)} * Applied by moves: {@linkcode Moves.HEAL_BLOCK | Heal Block (5 turns)}, {@linkcode Moves.PSYCHIC_NOISE | Psychic Noise (2 turns)}
* *
* @extends MoveRestrictionBattlerTag * @extends MoveRestrictionBattlerTag
*/ */
@ -2748,7 +2749,10 @@ export class HealBlockTag extends MoveRestrictionBattlerTag {
* @returns `true` if the move has a TRIAGE_MOVE flag and is a status move * @returns `true` if the move has a TRIAGE_MOVE flag and is a status move
*/ */
override isMoveRestricted(move: Moves): boolean { override isMoveRestricted(move: Moves): boolean {
return allMoves[move].hasFlag(MoveFlags.TRIAGE_MOVE) && allMoves[move].category === MoveCategory.STATUS; if (allMoves[move].hasFlag(MoveFlags.TRIAGE_MOVE) && allMoves[move].category === MoveCategory.STATUS) {
return true;
}
return false;
} }
/** /**
@ -3422,8 +3426,7 @@ export class PsychoShiftTag extends BattlerTag {
} }
/** /**
* Tag associated with {@linkcode Moves.MAGIC_COAT | Magic Coat} that reflects certain status moves directed at the user. * Tag associated with the move Magic Coat.
* TODO: Move Reflection code out of `move-effect-phase` and into here
*/ */
export class MagicCoatTag extends BattlerTag { export class MagicCoatTag extends BattlerTag {
constructor() { constructor() {

View File

@ -702,10 +702,10 @@ export default class Move implements Localizable {
/** /**
* Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move) * Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move)
* @param user - The {@linkcode Pokemon} using this move * @param user {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} targeted by this move * @param target {@linkcode Pokemon} target of the move
* @param move - The {@linkcode Move} being used * @param move {@linkcode Move} with this attribute
* @returns A string containing the custom failure text, or `undefined` if no custom text exists. * @returns string of the custom failure text, or `null` if it uses the default text ("But it failed!")
*/ */
getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined { getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
for (const attr of this.attrs) { for (const attr of this.attrs) {
@ -1186,7 +1186,7 @@ export class MoveEffectAttr extends MoveAttr {
*/ */
protected options?: MoveEffectAttrOptions; protected options?: MoveEffectAttrOptions;
constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) { constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) {
super(selfTarget); super(selfTarget);
this.options = options; this.options = options;
} }
@ -1373,8 +1373,8 @@ export class PreMoveMessageAttr extends MoveAttr {
/** /**
* Attribute for moves that can be conditionally interrupted to be considered to * Attribute for moves that can be conditionally interrupted to be considered to
* have failed before their "useMove" message is displayed. * have failed before their "useMove" message is displayed. Currently used by
* Currently used by {@linkcode Moves.FOCUS_PUNCH}. * Focus Punch.
* @extends MoveAttr * @extends MoveAttr
*/ */
export class PreUseInterruptAttr extends MoveAttr { export class PreUseInterruptAttr extends MoveAttr {
@ -1383,40 +1383,38 @@ export class PreUseInterruptAttr extends MoveAttr {
protected conditionFunc: MoveConditionFunc; protected conditionFunc: MoveConditionFunc;
/** /**
* Create a new PreUseInterruptAttr. * Create a new MoveInterruptedMessageAttr.
* @param message - Custom failure text to display when the move is interrupted, either as a string or a function producing one. * @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move.
* If ommitted, will display the default failure text upon cancellation.
* @param conditionFunc - A {@linkcode MoveConditionFunc} that returns `true` if the move should be canceled.
*/ */
constructor(message: string | undefined | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc: MoveConditionFunc) { constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) {
super(); super();
this.message = message; this.message = message;
this.conditionFunc = conditionFunc; this.conditionFunc = conditionFunc ?? (() => true);
} }
/** /**
* Conditionally cancel this pokemon's current move. * Message to display when a move is interrupted.
* @param user - The {@linkcode Pokemon} using this move * @param user {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} targeted by this move * @param target {@linkcode Pokemon} target of the move
* @param move - The {@linkcode Move} being used * @param move {@linkcode Move} with this attribute
* @returns `true` if the move should be cancelled.
*/ */
override apply(user: Pokemon, target: Pokemon, move: Move): boolean { override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
return this.conditionFunc(user, target, move); return this.conditionFunc(user, target, move);
} }
/** /**
* Obtain the text displayed upon this move's interruption. * Message to display when a move is interrupted.
* @param user - The {@linkcode Pokemon} using this move * @param user {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} targeted by this move * @param target {@linkcode Pokemon} target of the move
* @param move - The {@linkcode Move} being used * @param move {@linkcode Move} with this attribute
* @returns A string containing the custom failure text, or `undefined` if no custom text exists.
*/ */
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined { override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
if (this.conditionFunc(user, target, move)) { if (this.message && this.conditionFunc(user, target, move)) {
return typeof this.message !== "function" const message =
? this.message typeof this.message === "string"
: this.message(user, target, move); ? (this.message as string)
: this.message(user, target, move);
return message;
} }
} }
} }
@ -1516,7 +1514,7 @@ export class TargetHalfHpDamageAttr extends FixedDamageAttr {
case 0: case 0:
// first hit of move; update initialHp tracker // first hit of move; update initialHp tracker
this.initialHp = target.hp; this.initialHp = target.hp;
default: default:
// multi lens added hit; use initialHp tracker to ensure correct damage // multi lens added hit; use initialHp tracker to ensure correct damage
(args[0] as NumberHolder).value = toDmgValue(this.initialHp / 2); (args[0] as NumberHolder).value = toDmgValue(this.initialHp / 2);
return true; return true;
@ -3059,17 +3057,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
this.chargeText = chargeText; this.chargeText = chargeText;
} }
/** apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
* Apply the delayed attack, either setting it up or triggering the attack.
* @param user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} being targeted
* @param move - The {@linkcode Move} being used
* @param args -
* `[0]` - {@linkcode BooleanHolder} containing whether the move was overriden
* `[1]` - Whether the move is supposed to set up a delayed attack (`true`) or activate (`false`)
* @returns always `true`
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: [BooleanHolder, boolean]): boolean {
// Edge case for the move applied on a pokemon that has fainted // Edge case for the move applied on a pokemon that has fainted
if (!target) { if (!target) {
return true; return true;
@ -3105,9 +3093,9 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
/** /**
* If the user's ally is set to use a different move with this attribute, * If the user's ally is set to use a different move with this attribute,
* defer this move's effects for a combined move on the ally's turn. * defer this move's effects for a combined move on the ally's turn.
* @param user - The {@linkcode Pokemon} using the move * @param user the {@linkcode Pokemon} using this move
* @param target - Unused * @param target n/a
* @param move - The {@linkcode Move} being used * @param move the {@linkcode Move} being used
* @param args * @param args
* - [0] a {@linkcode BooleanHolder} indicating whether the move's base * - [0] a {@linkcode BooleanHolder} indicating whether the move's base
* effects should be overridden this turn. * effects should be overridden this turn.
@ -3696,7 +3684,7 @@ export class LessPPMorePowerAttr extends VariablePowerAttr {
const ppMax = move.pp; const ppMax = move.pp;
const ppUsed = user.moveset.find((m) => m.moveId === move.id)?.ppUsed ?? 0; const ppUsed = user.moveset.find((m) => m.moveId === move.id)?.ppUsed ?? 0;
let ppRemains = ppMax - ppUsed; let ppRemains = ppMax - ppUsed;
/** Reduce to 0 to avoid negative numbers if user has 1PP before attack and target has Ability.PRESSURE */ /** Reduce to 0 to avoid negative numbers if user has 1PP before attack and target has Ability.PRESSURE */
if (ppRemains < 0) { if (ppRemains < 0) {
ppRemains = 0; ppRemains = 0;
@ -3814,30 +3802,26 @@ export class DoublePowerChanceAttr extends VariablePowerAttr {
export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr { export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr {
constructor(limit: number, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) { constructor(limit: number, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) {
super((user: Pokemon, _target: Pokemon, move: Move): number => { super((user: Pokemon, target: Pokemon, move: Move): number => {
const moveHistory = user.getLastXMoves(-1).slice(1, limit+1); // don't count the first history entry (ie the current move) const moveHistory = user.getLastXMoves(limit + 1).slice(1);
let count = 1; let count = 0;
let turnMove: TurnMove | undefined;
// TODO: Confirm whether mirror moving an echoed voice counts for and/or resets a boost while (
for (const tm of moveHistory) { (
if ( (turnMove = moveHistory.shift())?.move === move.id
!(tm.move === move.id || comboMoves.includes(tm.move)) || (comboMoves.length && comboMoves.includes(turnMove?.move ?? Moves.NONE))
|| (resetOnFail && tm.result !== MoveResult.SUCCESS) )
) { && (!resetOnFail || turnMove?.result === MoveResult.SUCCESS)
) {
if (count < (limit - 1)) {
count++;
} else if (resetOnLimit) {
count = 0;
} else {
break; break;
} }
if (count < limit - 1) {
count++;
continue;
}
if (resetOnLimit) {
count = 0;
}
break;
} }
return this.getMultiplier(count); return this.getMultiplier(count);
@ -4017,7 +4001,7 @@ export class HpPowerAttr extends VariablePowerAttr {
/** /**
* Attribute used for moves whose base power scales with the opponent's HP * Attribute used for moves whose base power scales with the opponent's HP
* Used for {@linkcode Moves.CRUSH_GRIP}, {@linkcode Moves.WRING_OUT}, and {@linkcode Moves.HARD_PRESS} * Used for Crush Grip, Wring Out, and Hard Press
* maxBasePower 100 for Hard Press, 120 for others * maxBasePower 100 for Hard Press, 120 for others
*/ */
export class OpponentHighHpPowerAttr extends VariablePowerAttr { export class OpponentHighHpPowerAttr extends VariablePowerAttr {
@ -4333,7 +4317,6 @@ const hasStockpileStacksCondition: MoveConditionFunc = (user) => {
return !!hasStockpilingTag && hasStockpilingTag.stockpiledCount > 0; return !!hasStockpilingTag && hasStockpilingTag.stockpiledCount > 0;
}; };
/** /**
* Attribute used for multi-hit moves that increase power in increments of the * Attribute used for multi-hit moves that increase power in increments of the
* move's base power for each hit, namely Triple Kick and Triple Axel. * move's base power for each hit, namely Triple Kick and Triple Axel.
@ -5434,17 +5417,11 @@ export class FrenzyAttr extends MoveEffectAttr {
} }
// TODO: Disable if used via dancer // TODO: Disable if used via dancer
// TODO: Add support for moves that don't add the frenzy tag (Uproar, Rollout, etc.) // TODO: Add support for moves that don't add the frenzy tag (Uproar, Rollout, etc.)
// If frenzy is not in effect and we don't have anything queued up, if (!user.getTag(BattlerTagType.FRENZY) && !user.getMoveQueue().length) {
// add 1-2 extra instances of the move to the move queue. const turnCount = user.randSeedIntRange(1, 2);
// If frenzy is already in effect, tick down the tag. new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }));
if (!user.getTag(BattlerTagType.FRENZY) && user.getMoveQueue().length === 0) {
const turnCount = user.randSeedIntRange(1, 2); // excludes initial use
for (let i = 0; i < turnCount; i++) {
user.pushMoveQueue({ move: move.id, targets: [ target.getBattlerIndex() ], useType: MoveUseType.IGNORE_PP });
}
user.addTag(BattlerTagType.FRENZY, turnCount, move.id, user.id); user.addTag(BattlerTagType.FRENZY, turnCount, move.id, user.id);
} else { } else {
applyMoveAttrs(AddBattlerTagAttr, user, target, move, args); applyMoveAttrs(AddBattlerTagAttr, user, target, move, args);
@ -5817,24 +5794,23 @@ export class ProtectAttr extends AddBattlerTagAttr {
super(tagType, true); super(tagType, true);
} }
/**
* Condition to fail a protect usage based on random chance.
* Chance starts at 100% and is thirded for each prior successful proctect usage.
* @returns a function that fails the function if its proc chance roll fails
*/
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return ((user, target, move): boolean => { return ((user, target, move): boolean => {
const lastMoves = user.getLastXMoves(-1) let timesUsed = 0;
const moveHistory = user.getLastXMoves();
let turnMove: TurnMove | undefined;
let threshold = 1; while (moveHistory.length) {
for (const tm of lastMoves) { turnMove = moveHistory.shift();
if (!allMoves[tm.move].hasAttr(ProtectAttr) || tm.result !== MoveResult.SUCCESS) { if (!allMoves[turnMove?.move ?? Moves.NONE].hasAttr(ProtectAttr) || turnMove?.result !== MoveResult.SUCCESS) {
break; break;
} }
threshold *= 3; timesUsed++;
} }
if (timesUsed) {
return threshold === 1 || user.randSeedInt(threshold) === 0; return !user.randSeedInt(Math.pow(3, timesUsed));
}
return true;
}); });
} }
} }
@ -7334,14 +7310,13 @@ export class SketchAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true); super(true);
} }
/** /**
* User copies the opponent's last used move, if possible. * User copies the opponent's last used move, if possible
* @param user - The {@linkcode Pokemon} using the move * @param {Pokemon} user Pokemon that used the move and will replace Sketch with the copied move
* @param target - The {@linkcode Pokemon} being targeted by the move * @param {Pokemon} target Pokemon that the user wants to copy a move from
* @param move - The {@linkcoed Move} being used * @param {Move} move Move being used
* @param args - Unused * @param {any[]} args Unused
* @returns Whether a move was successfully learnt * @returns {boolean} true if the function succeeds, otherwise false
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -7881,6 +7856,7 @@ export class AfterYouAttr extends MoveEffectAttr {
/** /**
* Move effect to force the target to move last, ignoring priority. * Move effect to force the target to move last, ignoring priority.
* If applied to multiple targets, they move in speed order after all other moves. * If applied to multiple targets, they move in speed order after all other moves.
* @extends MoveEffectAttr
*/ */
export class ForceLastAttr extends MoveEffectAttr { export class ForceLastAttr extends MoveEffectAttr {
/** /**
@ -7927,7 +7903,7 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean
let slower: boolean; let slower: boolean;
// quashed pokemon still have speed ties // quashed pokemon still have speed ties
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) { if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
slower = !target.randSeedInt(2); slower = target.randSeedInt(2) === 0;
} else { } else {
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD); slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
} }

View File

@ -642,7 +642,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Checks if a pokemon is fainted (ie: its `hp <= 0`). * Checks if a pokemon is fainted (ie: its `hp <= 0`).
* It's usually better to call {@linkcode isAllowedInBattle()} * It's usually better to call {@linkcode isAllowedInBattle()}
* @param checkStatus - Whether to also check the pokemon's status for {@linkcode StatusEffect.FAINT}; default `false` * @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT}
* @returns `true` if the pokemon is fainted * @returns `true` if the pokemon is fainted
*/ */
public isFainted(checkStatus = false): boolean { public isFainted(checkStatus = false): boolean {
@ -3256,9 +3256,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck. * If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck.
* *
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
* @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
* @param applyModifiersToOverride - Whether to apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}. * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}
* Does nothing if {@linkcode thresholdOverride} is not set.
* @returns `true` if the Pokemon has been set as a shiny, `false` otherwise * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise
*/ */
public trySetShinySeed( public trySetShinySeed(
@ -3812,7 +3811,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
ui => ui =>
ui instanceof BattleInfo && ui instanceof BattleInfo &&
(ui as BattleInfo) instanceof PlayerBattleInfo === this.isPlayer(), (ui as BattleInfo) instanceof PlayerBattleInfo === this.isPlayer(),
)[0] as Phaser.GameObjects.GameObject | undefined; )
.find(() => true);
if (!otherBattleInfo || !this.getFieldIndex()) { if (!otherBattleInfo || !this.getFieldIndex()) {
globalScene.fieldUI.sendToBack(this.battleInfo); globalScene.fieldUI.sendToBack(this.battleInfo);
globalScene.sendTextToBack(); // Push the top right text objects behind everything else globalScene.sendTextToBack(); // Push the top right text objects behind everything else
@ -5140,8 +5140,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Returns a list of the most recent move entries in this Pokemon's move history. * Returns a list of the most recent move entries in this Pokemon's move history.
* The retrieved move entries are sorted in order from NEWEST to OLDEST. * The retrieved move entries are sorted in order from NEWEST to OLDEST.
* @param moveCount The number of move entries to retrieve.\ * @param moveCount The number of move entries to retrieve.
* If negative, retrieves the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}). * If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}).
* Default is `1`. * Default is `1`.
* @returns A list of {@linkcode TurnMove}, as specified above. * @returns A list of {@linkcode TurnMove}, as specified above.
*/ */
@ -5660,19 +5660,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (effect === StatusEffect.SLEEP) { if (effect === StatusEffect.SLEEP) {
sleepTurnsRemaining = new NumberHolder(this.randSeedIntRange(2, 4)); sleepTurnsRemaining = new NumberHolder(this.randSeedIntRange(2, 4));
this.setFrameRate(4); this.setFrameRate(4);
// If the user is invulnerable, remove their invulnerability when they fall asleep // If the user is invulnerable, lets remove their invulnerability when they fall asleep
// and remove the upcoming attack from the move queue. const invulnerableTags = [
const tag = [
BattlerTagType.UNDERGROUND, BattlerTagType.UNDERGROUND,
BattlerTagType.UNDERWATER, BattlerTagType.UNDERWATER,
BattlerTagType.HIDDEN, BattlerTagType.HIDDEN,
BattlerTagType.FLYING, BattlerTagType.FLYING,
].find(t => this.getTag(t)); ];
const tag = invulnerableTags.find(t => this.getTag(t));
if (tag) { if (tag) {
this.removeTag(tag); this.removeTag(tag);
this.getMoveQueue().shift(); this.getMoveQueue().pop();
} }
} }
@ -8137,10 +8140,10 @@ export class PokemonMove {
} }
/** /**
* Increments this move's {@linkcode ppUsed} variable (up to a maximum of {@link getMovePp}). * Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp}
* @param count - Amount of PP to consume; default `1` * @param count Amount of PP to use
*/ */
usePp(count = 1) { usePp(count: number = 1) {
this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp()); this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp());
} }

View File

@ -135,11 +135,11 @@ export class MoveEffectPhase extends PokemonPhase {
* Compute targets and the results of hit checks of the invoked move against all targets, * Compute targets and the results of hit checks of the invoked move against all targets,
* organized by battler index. * organized by battler index.
* *
* **This is *not* a pure function** and has the following side effects: * **This is *not* a pure function**; it has the following side effects
* - Sets `this.hitChecks` to the results of the hit checks against each target * - `this.hitChecks` - The results of the hit checks against each target
* - Sets success/failure of `this.moveHistoryEntry` based on the hit check results * - `this.moveHistoryEntry` - Sets success or failure based on the hit check results
* - Sets `user.turnData.hitCount` and `user.turnData.hitsLeft` to 1 if the move * - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the
* was unsuccessful against all targets (effectively canceling it) * move was unsuccessful against all targets
* *
* @returns The targets of the invoked move * @returns The targets of the invoked move
* @see {@linkcode hitCheck} * @see {@linkcode hitCheck}
@ -205,9 +205,9 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** /**
* Apply the move to each of its resolved targets. * Apply the move to each of the resolved targets.
* @param targets - The resolved set of targets of the move * @param targets - The resolved set of targets of the move
* @throws - Error if there was an unexpected hit check result * @throws Error if there was an unexpected hit check result
*/ */
private applyToTargets(user: Pokemon, targets: Pokemon[]): void { private applyToTargets(user: Pokemon, targets: Pokemon[]): void {
for (const [i, target] of targets.entries()) { for (const [i, target] of targets.entries()) {
@ -229,7 +229,6 @@ export class MoveEffectPhase extends PokemonPhase {
case HitCheckResult.NO_EFFECT_NO_MESSAGE: case HitCheckResult.NO_EFFECT_NO_MESSAGE:
case HitCheckResult.PROTECTED: case HitCheckResult.PROTECTED:
case HitCheckResult.TARGET_NOT_ON_FIELD: case HitCheckResult.TARGET_NOT_ON_FIELD:
// Apply effects for ineffective moves (e.g. High Jump Kick crash dmg)
applyMoveAttrs(NoEffectAttr, user, target, this.move); applyMoveAttrs(NoEffectAttr, user, target, this.move);
break; break;
case HitCheckResult.MISS: case HitCheckResult.MISS:
@ -643,19 +642,22 @@ export class MoveEffectPhase extends PokemonPhase {
if (!user) { if (!user) {
return false; return false;
} }
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
switch (true) { return true;
// No Guard }
case user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr): if (this.move.hasAttr(ToxicAccuracyAttr) && user.isOfType(PokemonType.POISON)) {
// Toxic as poison type return true;
case this.move.hasAttr(ToxicAccuracyAttr) && user.isOfType(PokemonType.POISON): }
// Lock On/Mind Reader // TODO: Fix lock on / mind reader check.
case !!user.getTag(BattlerTagType.IGNORE_ACCURACY): if (
// Spikes and company user.getTag(BattlerTagType.IGNORE_ACCURACY) &&
case isFieldTargeted(this.move): (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1
return true; ) {
return true;
}
if (isFieldTargeted(this.move)) {
return true;
} }
return false;
} }
/** /**

View File

@ -27,16 +27,16 @@ export class TurnStartPhase extends FieldPhase {
/** /**
* This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array. * This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array.
* It also checks for Trick Room and reverses the array if it is present. * It also checks for Trick Room and reverses the array if it is present.
* @returns An array of {@linkcode BattlerIndex}es containing all on-field pokemon sorted in speed order. * @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed
*/ */
getSpeedOrder(): BattlerIndex[] { getSpeedOrder(): BattlerIndex[] {
const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[]; const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[]; const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
// Shuffle the list before sorting so speed ties produce random results // We shuffle the list before sorting so speed ties produce random results
// This is seeded with the current turn to prevent an inconsistency with variable turn order
// based on how long since you last reloaded
let orderedTargets: Pokemon[] = playerField.concat(enemyField); let orderedTargets: Pokemon[] = playerField.concat(enemyField);
// We seed it with the current turn to prevent an inconsistency where it
// was varying based on how long since you last reloaded
globalScene.executeWithSeedOffset( globalScene.executeWithSeedOffset(
() => { () => {
orderedTargets = randSeedShuffle(orderedTargets); orderedTargets = randSeedShuffle(orderedTargets);
@ -45,11 +45,11 @@ export class TurnStartPhase extends FieldPhase {
globalScene.waveSeed, globalScene.waveSeed,
); );
// Check for Trick Room and reverse sort order if active. // Next, a check for Trick Room is applied to determine sort order.
// Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd.
const speedReversed = new BooleanHolder(false); const speedReversed = new BooleanHolder(false);
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
// Adjust the sort function based on whether Trick Room is active.
orderedTargets.sort((a: Pokemon, b: Pokemon) => { orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0; const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0; const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
@ -120,8 +120,7 @@ export class TurnStartPhase extends FieldPhase {
} }
} }
// If there is no difference between the move's calculated priorities, // If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result.
// check for differences in battlerBypassSpeed and returns the result.
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
return battlerBypassSpeed[a].value ? -1 : 1; return battlerBypassSpeed[a].value ? -1 : 1;
} }

View File

@ -44,6 +44,7 @@ describe("Abilities - Wimp Out", () => {
function confirmSwitch(): void { function confirmSwitch(): void {
const [pokemon1, pokemon2] = game.scene.getPlayerParty(); const [pokemon1, pokemon2] = game.scene.getPlayerParty();
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD); expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD);
@ -55,34 +56,17 @@ describe("Abilities - Wimp Out", () => {
function confirmNoSwitch(): void { function confirmNoSwitch(): void {
const [pokemon1, pokemon2] = game.scene.getPlayerParty(); const [pokemon1, pokemon2] = game.scene.getPlayerParty();
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD);
expect(pokemon1.species.speciesId).toBe(Species.WIMPOD); expect(pokemon1.species.speciesId).toBe(Species.WIMPOD);
expect(pokemon1.isFainted()).toBe(false); expect(pokemon1.isFainted()).toBe(false);
expect(pokemon1.getHpRatio()).toBeLessThan(0.5); expect(pokemon1.getHpRatio()).toBeLessThan(0.5);
expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD);
} }
it("should switch user out when falling below 50% HP, cancelling any pending moves", async () => { it("triggers regenerator passive single time when switching out with wimp out", async () => {
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("DamageAnimPhase", false);
game.phaseInterceptor.clearLogs();
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
expect(wimpod.turnData.acted).toBe(false);
expect(game.phaseInterceptor.log).not.toContain("MoveEffectPhase");
});
it("should trigger regenerator passive when switching out", async () => {
game.override.passiveAbility(Abilities.REGENERATOR).startingLevel(5).enemyLevel(100); game.override.passiveAbility(Abilities.REGENERATOR).startingLevel(5).enemyLevel(100);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -96,7 +80,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); confirmSwitch();
}); });
it("should cause wild pokemon to flee", async () => { it("It makes wild pokemon flee if triggered", async () => {
game.override.enemyAbility(Abilities.WIMP_OUT); game.override.enemyAbility(Abilities.WIMP_OUT);
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
@ -106,11 +90,12 @@ describe("Abilities - Wimp Out", () => {
game.move.select(Moves.FALSE_SWIPE); game.move.select(Moves.FALSE_SWIPE);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.visible).toBe(false); const isVisible = enemyPokemon.visible;
expect(enemyPokemon.switchOutStatus).toBe(true); const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true);
}); });
it("should not trigger when HP is already below half", async () => { it("Does not trigger when HP already below half", async () => {
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!; const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp = 5; wimpod.hp = 5;
@ -122,7 +107,7 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch(); confirmNoSwitch();
}); });
it("should bypass trapping moves and abilities", async () => { it("Trapping moves do not prevent Wimp Out from activating.", async () => {
game.override.enemyMoveset([Moves.SPIRIT_SHACKLE]).startingLevel(53).enemyLevel(45); game.override.enemyMoveset([Moves.SPIRIT_SHACKLE]).startingLevel(53).enemyLevel(45);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -137,7 +122,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); confirmSwitch();
}); });
it("should block switching from U-Turn on activation", async () => { it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => {
game.override.startingLevel(95).enemyMoveset([Moves.U_TURN]); game.override.startingLevel(95).enemyMoveset([Moves.U_TURN]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -151,7 +136,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); confirmSwitch();
}); });
it("should not block switching from U-Turn on failed activation", async () => { it("If this Ability does not activate due to being hit by U-turn or Volt Switch, the user of that move will be switched out.", async () => {
game.override.startingLevel(190).startingWave(8).enemyMoveset([Moves.U_TURN]); game.override.startingLevel(190).startingWave(8).enemyMoveset([Moves.U_TURN]);
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id; const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id;
@ -160,7 +145,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1); expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1);
}); });
it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates", async () => { it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => {
game.override.startingLevel(69).enemyMoveset([Moves.DRAGON_TAIL]); game.override.startingLevel(69).enemyMoveset([Moves.DRAGON_TAIL]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -177,7 +162,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD); expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD);
}); });
it("should trigger from recoil damage", async () => { it("triggers when recoil damage is taken", async () => {
game.override.moveset([Moves.HEAD_SMASH]).enemyMoveset([Moves.SPLASH]); game.override.moveset([Moves.HEAD_SMASH]).enemyMoveset([Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -188,7 +173,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); confirmSwitch();
}); });
it("should not activate when the Pokémon cuts its own HP", async () => { it("It does not activate when the Pokémon cuts its own HP", async () => {
game.override.moveset([Moves.SUBSTITUTE]).enemyMoveset([Moves.SPLASH]); game.override.moveset([Moves.SUBSTITUTE]).enemyMoveset([Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -201,19 +186,7 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch(); confirmNoSwitch();
}); });
it("should not trigger from Sheer Force-boosted moves", async () => { it("Does not trigger when neutralized", async () => {
game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("should not trigger while neutralized", async () => {
game.override.enemyAbility(Abilities.NEUTRALIZING_GAS).startingLevel(5); game.override.enemyAbility(Abilities.NEUTRALIZING_GAS).startingLevel(5);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -250,140 +223,106 @@ describe("Abilities - Wimp Out", () => {
}, },
); );
// TODO: Condense into it.eaches it("Wimp Out will activate due to weather damage", async () => {
describe("Post Turn Damage Checks - ", () => { game.override.weather(WeatherType.HAIL).enemyMoveset([Moves.SPLASH]);
beforeEach(() => { await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.override.enemyMoveset(Moves.SPLASH);
});
it("Wimp Out will activate due to weather damage", async () => { game.scene.getPlayerPokemon()!.hp *= 0.51;
game.override.weather(WeatherType.HAIL);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51; game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.SPLASH); confirmSwitch();
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
it("Wimp Out will activate due to post turn status damage", async () => {
game.override.statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to leech seed", async () => {
game.override.enemyMoveset([Moves.LEECH_SEED]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to curse damage", async () => {
game.override.enemySpecies(Species.DUSKNOIR).enemyMoveset([Moves.CURSE]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to salt cure damage", async () => {
game.override.enemySpecies(Species.NACLI).enemyMoveset([Moves.SALT_CURE]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.7;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to damaging trap damage", async () => {
game.override.enemySpecies(Species.MAGIKARP).enemyMoveset([Moves.WHIRLPOOL]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.55;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to aftermath", async () => {
game.override
.moveset([Moves.THUNDER_PUNCH])
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.AFTERMATH)
.enemyMoveset([Moves.SPLASH])
.enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.THUNDER_PUNCH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
it("Wimp Out will activate due to bad dreams", async () => {
game.override.statusEffect(StatusEffect.SLEEP).enemyAbility(Abilities.BAD_DREAMS);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Activates due to entry hazards", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override.enemySpecies(Species.CENTISKORCH).enemyAbility(Abilities.WIMP_OUT).startingWave(4);
await game.classicMode.startBattle([Species.TYRUNT]);
expect(game.phaseInterceptor.log).not.toContain("MovePhase");
expect(game.phaseInterceptor.log).toContain("BattleEndPhase");
});
it("Wimp Out will activate due to Nightmare", async () => {
game.override.enemyMoveset([Moves.NIGHTMARE]).statusEffect(StatusEffect.SLEEP);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.65;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
}); });
it("should not trigger on Magic Guard-prevented damage", async () => { it("Does not trigger when enemy has sheer force", async () => {
game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("Wimp Out will activate due to post turn status damage", async () => {
game.override.statusEffect(StatusEffect.POISON).enemyMoveset([Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to bad dreams", async () => {
game.override.statusEffect(StatusEffect.SLEEP).enemyAbility(Abilities.BAD_DREAMS);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to leech seed", async () => {
game.override.enemyMoveset([Moves.LEECH_SEED]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to curse damage", async () => {
game.override.enemySpecies(Species.DUSKNOIR).enemyMoveset([Moves.CURSE]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to salt cure damage", async () => {
game.override.enemySpecies(Species.NACLI).enemyMoveset([Moves.SALT_CURE]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.7;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to damaging trap damage", async () => {
game.override.enemySpecies(Species.MAGIKARP).enemyMoveset([Moves.WHIRLPOOL]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.55;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override game.override
@ -392,7 +331,6 @@ describe("Abilities - Wimp Out", () => {
.weather(WeatherType.HAIL) .weather(WeatherType.HAIL)
.statusEffect(StatusEffect.POISON); .statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51; game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
@ -403,19 +341,6 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD);
}); });
it("triggers status on the wimp out user before a new pokemon is switched in", async () => {
game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100);
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerParty()[1].status?.effect).toEqual(StatusEffect.POISON);
confirmSwitch();
});
it("Wimp Out activating should not cancel a double battle", async () => { it("Wimp Out activating should not cancel a double battle", async () => {
game.override.battleStyle("double").enemyAbility(Abilities.WIMP_OUT).enemyMoveset([Moves.SPLASH]).enemyLevel(1); game.override.battleStyle("double").enemyAbility(Abilities.WIMP_OUT).enemyMoveset([Moves.SPLASH]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -436,7 +361,59 @@ describe("Abilities - Wimp Out", () => {
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp()); expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
}); });
it("triggers after last hit of multi hit moves", async () => { it("Wimp Out will activate due to aftermath", async () => {
game.override
.moveset([Moves.THUNDER_PUNCH])
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.AFTERMATH)
.enemyMoveset([Moves.SPLASH])
.enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.THUNDER_PUNCH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
it("Activates due to entry hazards", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override.enemySpecies(Species.CENTISKORCH).enemyAbility(Abilities.WIMP_OUT).startingWave(4);
await game.classicMode.startBattle([Species.TYRUNT]);
expect(game.phaseInterceptor.log).not.toContain("MovePhase");
expect(game.phaseInterceptor.log).toContain("BattleEndPhase");
});
it("Wimp Out will activate due to Nightmare", async () => {
game.override.enemyMoveset([Moves.NIGHTMARE]).statusEffect(StatusEffect.SLEEP);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.65;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("triggers status on the wimp out user before a new pokemon is switched in", async () => {
game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100);
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerParty()[1].status?.effect).toEqual(StatusEffect.POISON);
confirmSwitch();
});
it("triggers after last hit of multi hit move", async () => {
game.override.enemyMoveset(Moves.BULLET_SEED).enemyAbility(Abilities.SKILL_LINK); game.override.enemyMoveset(Moves.BULLET_SEED).enemyAbility(Abilities.SKILL_LINK);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -467,7 +444,6 @@ describe("Abilities - Wimp Out", () => {
expect(enemyPokemon.turnData.hitCount).toBe(2); expect(enemyPokemon.turnData.hitCount).toBe(2);
confirmSwitch(); confirmSwitch();
}); });
it("triggers after last hit of Parental Bond", async () => { it("triggers after last hit of Parental Bond", async () => {
game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.PARENTAL_BOND); game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.PARENTAL_BOND);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);