From da6443562bc9ea8b0e7d6427cca43f1d842a10c9 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 10 May 2025 16:28:22 -0400 Subject: [PATCH] Reverted a couple doc changes --- src/data/abilities/ability.ts | 38 ++-- src/data/battler-tags.ts | 59 +++--- src/data/moves/move.ts | 164 +++++++-------- src/field/pokemon.ts | 33 +-- src/phases/move-effect-phase.ts | 42 ++-- src/phases/turn-start-phase.ts | 15 +- test/abilities/wimp_out.test.ts | 354 +++++++++++++++----------------- 7 files changed, 332 insertions(+), 373 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 6a44e01b415..413a8efc136 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -2335,18 +2335,18 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { // phase list (which could be after CommandPhase for example) globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, this.stats, this.stages)); } else { - for (const opponent of pokemon.getOpponents()) { - const cancelled = new BooleanHolder(false); - if (this.intimidate) { - applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated); - applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated); + for (const opponent of pokemon.getOpponents()) { + const cancelled = new BooleanHolder(false); + if (this.intimidate) { + applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated); + applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated); - if (opponent.getTag(BattlerTagType.SUBSTITUTE)) { - cancelled.value = true; + if (opponent.getTag(BattlerTagType.SUBSTITUTE)) { + cancelled.value = true; + } } - } - if (!cancelled.value) { - globalScene.unshiftPhase(new StatStageChangePhase(opponent.getBattlerIndex(), false, this.stats, this.stages)); + if (!cancelled.value) { + globalScene.unshiftPhase(new StatStageChangePhase(opponent.getBattlerIndex(), false, this.stats, this.stages)); } } } @@ -4399,12 +4399,12 @@ export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr { * @extends AbAttr */ export class PostMoveUsedAbAttr extends AbAttr { - canApplyPostMoveUsed( + canApplyPostMoveUsed( pokemon: Pokemon, move: PokemonMove, source: Pokemon, targets: BattlerIndex[], - simulated: boolean, + simulated: boolean, args: any[]): boolean { return true; } @@ -4414,7 +4414,7 @@ export class PostMoveUsedAbAttr extends AbAttr { move: PokemonMove, source: Pokemon, targets: BattlerIndex[], - simulated: boolean, + simulated: boolean, args: any[], ): void {} } @@ -4899,7 +4899,7 @@ export class BlockRedirectAbAttr extends AbAttr { } * @see {@linkcode apply} */ export class ReduceStatusEffectDurationAbAttr extends AbAttr { -private statusEffect: StatusEffect; + private statusEffect: StatusEffect; constructor(statusEffect: StatusEffect) { super(false); @@ -4908,7 +4908,7 @@ private statusEffect: StatusEffect; } 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 (switchOutTarget instanceof PlayerPokemon) { - if (globalScene.getPlayerParty().every(p => !p.isActive(true))) { + if (globalScene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; } @@ -5890,7 +5890,7 @@ export function applyPostMoveUsedAbAttrs( move: PokemonMove, source: Pokemon, targets: BattlerIndex[], - simulated = false, + simulated = false, ...args: any[] ): void { applyAbAttrsInternal( @@ -6901,8 +6901,8 @@ export function initAbilities() { new Ability(Abilities.ANALYTIC, 5) .attr(MovePowerBoostAbAttr, (user, target, move) => { // 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); - return isNullOrUndefined(laterMovePhase); + const movePhase = globalScene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user?.id); + return isNullOrUndefined(movePhase); }, 1.3), new Ability(Abilities.ILLUSION, 5) // The Pokemon generate an illusion if it's available diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 1eb3b9efbae..120cb3aa701 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -51,10 +51,6 @@ export enum BattlerTagLapseType { MOVE, PRE_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, TURN_END, HIT, @@ -65,7 +61,7 @@ export enum BattlerTagLapseType { export class BattlerTag { public tagType: BattlerTagType; - public lapseTypes: BattlerTagLapseType[]; // TODO: Make this a set + public lapseTypes: BattlerTagLapseType[]; public turnCount: number; public sourceMove: Moves; public sourceId?: number; @@ -98,7 +94,7 @@ export class BattlerTag { 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`) */ 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 user - The {@linkcode Pokemon} using the move. - * @returns Whether the given move is restricted by this tag. + * @param move - {@linkcode Moves} ID to check restriction for. + * @param user - The {@linkcode Pokemon} involved + * @returns `true` if the move is restricted by this tag, otherwise `false`. */ public abstract isMoveRestricted(move: Moves, user?: Pokemon): boolean; /** - * Check if this tag is restricting a move during target selection. - * Returns `false` by default unless overridden by a child class. - * @param _move - The {@linkcode Moves | move ID} whose selectability is being checked - * @param _user - The {@linkcode Pokemon} using the move. - * @param _target - The {@linkcode Pokemon} being targeted - * @returns Whether the given move should be unselectable when choosing targets. + * Checks if this tag is restricting a move based on a user's decisions during the target selection phase + * + * @param {Moves} _move {@linkcode Moves} move ID to check restriction for + * @param {Pokemon} _user {@linkcode Pokemon} the user of the above move + * @param {Pokemon} _target {@linkcode Pokemon} the target of the above move + * @returns {boolean} `false` unless overridden by the child tag */ isMoveTargetRestricted(_move: Moves, _user: Pokemon, _target: Pokemon): boolean { return false; @@ -346,10 +342,9 @@ export class DisabledTag extends MoveRestrictionBattlerTag { /** * @override - * Display the text that occurs when a move is interrupted via Disable. - * @param pokemon - The {@linkcode Pokemon} attempting to use the restricted move - * @param move - The {@linkcode Moves | move ID} of the move being interrupted - * @returns The text to display when the given move is interrupted + * @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move + * @param {Moves} move {@linkcode Moves} ID of the move being interrupted + * @returns {string} text to display when the move is interrupted */ override interruptedText(pokemon: Pokemon, move: Moves): string { 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. + * @extends MoveRestrictionBattlerTag */ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { private moveId = Moves.NONE; @@ -416,10 +412,11 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { } /** + * * @override - * @param pokemon - The {@linkcode Pokemon} attempting to select a move - * @param _move - Unused - * @returns The text to display when the move is rendered unselectable + * @param {Pokemon} pokemon n/a + * @param {Moves} _move {@linkcode Moves} ID of the move being denied + * @returns {string} text to display when the move is denied */ override selectionDeniedText(pokemon: Pokemon, _move: Moves): string { return i18next.t("battle:canOnlyUseMove", { @@ -640,7 +637,7 @@ class NoRetreatTag extends TrappedTag { */ export class FlinchedTag extends BattlerTag { 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 { @@ -1481,6 +1478,10 @@ export class WrapTag 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 { return i18next.t("battlerTags:vortexOnTrap", { 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. - * 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 */ @@ -2748,7 +2749,10 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { * @returns `true` if the move has a TRIAGE_MOVE flag and is a status move */ 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. - * TODO: Move Reflection code out of `move-effect-phase` and into here + * Tag associated with the move Magic Coat. */ export class MagicCoatTag extends BattlerTag { constructor() { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 83bd204865c..c40a9f64164 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -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) - * @param user - The {@linkcode Pokemon} using this move - * @param target - The {@linkcode Pokemon} targeted by this move - * @param move - The {@linkcode Move} being used - * @returns A string containing the custom failure text, or `undefined` if no custom text exists. + * @param user {@linkcode Pokemon} using the move + * @param target {@linkcode Pokemon} target of the move + * @param move {@linkcode Move} with this attribute + * @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 { for (const attr of this.attrs) { @@ -1186,7 +1186,7 @@ export class MoveEffectAttr extends MoveAttr { */ protected options?: MoveEffectAttrOptions; - constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) { + constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) { super(selfTarget); this.options = options; } @@ -1373,8 +1373,8 @@ export class PreMoveMessageAttr extends MoveAttr { /** * Attribute for moves that can be conditionally interrupted to be considered to - * have failed before their "useMove" message is displayed. - * Currently used by {@linkcode Moves.FOCUS_PUNCH}. + * have failed before their "useMove" message is displayed. Currently used by + * Focus Punch. * @extends MoveAttr */ export class PreUseInterruptAttr extends MoveAttr { @@ -1383,40 +1383,38 @@ export class PreUseInterruptAttr extends MoveAttr { protected conditionFunc: MoveConditionFunc; /** - * Create a new PreUseInterruptAttr. - * @param message - Custom failure text to display when the move is interrupted, either as a string or a function producing one. - * If ommitted, will display the default failure text upon cancellation. - * @param conditionFunc - A {@linkcode MoveConditionFunc} that returns `true` if the move should be canceled. + * Create a new MoveInterruptedMessageAttr. + * @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. */ - 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(); this.message = message; - this.conditionFunc = conditionFunc; + this.conditionFunc = conditionFunc ?? (() => true); } /** - * Conditionally cancel this pokemon's current move. - * @param user - The {@linkcode Pokemon} using this move - * @param target - The {@linkcode Pokemon} targeted by this move - * @param move - The {@linkcode Move} being used - * @returns `true` if the move should be cancelled. + * Message to display when a move is interrupted. + * @param user {@linkcode Pokemon} using the move + * @param target {@linkcode Pokemon} target of the move + * @param move {@linkcode Move} with this attribute */ override apply(user: Pokemon, target: Pokemon, move: Move): boolean { return this.conditionFunc(user, target, move); } /** - * Obtain the text displayed upon this move's interruption. - * @param user - The {@linkcode Pokemon} using this move - * @param target - The {@linkcode Pokemon} targeted by this move - * @param move - The {@linkcode Move} being used - * @returns A string containing the custom failure text, or `undefined` if no custom text exists. + * Message to display when a move is interrupted. + * @param user {@linkcode Pokemon} using the move + * @param target {@linkcode Pokemon} target of the move + * @param move {@linkcode Move} with this attribute */ override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined { - if (this.conditionFunc(user, target, move)) { - return typeof this.message !== "function" - ? this.message - : this.message(user, target, move); + if (this.message && this.conditionFunc(user, target, move)) { + const message = + typeof this.message === "string" + ? (this.message as string) + : this.message(user, target, move); + return message; } } } @@ -1516,7 +1514,7 @@ export class TargetHalfHpDamageAttr extends FixedDamageAttr { case 0: // first hit of move; update initialHp tracker this.initialHp = target.hp; - default: + default: // multi lens added hit; use initialHp tracker to ensure correct damage (args[0] as NumberHolder).value = toDmgValue(this.initialHp / 2); return true; @@ -3059,17 +3057,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr { this.chargeText = chargeText; } - /** - * 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 { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { // Edge case for the move applied on a pokemon that has fainted if (!target) { 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, * defer this move's effects for a combined move on the ally's turn. - * @param user - The {@linkcode Pokemon} using the move - * @param target - Unused - * @param move - The {@linkcode Move} being used + * @param user the {@linkcode Pokemon} using this move + * @param target n/a + * @param move the {@linkcode Move} being used * @param args * - [0] a {@linkcode BooleanHolder} indicating whether the move's base * effects should be overridden this turn. @@ -3696,7 +3684,7 @@ export class LessPPMorePowerAttr extends VariablePowerAttr { const ppMax = move.pp; 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 */ if (ppRemains < 0) { ppRemains = 0; @@ -3814,30 +3802,26 @@ export class DoublePowerChanceAttr extends VariablePowerAttr { export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr { constructor(limit: number, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) { - 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) + super((user: Pokemon, target: Pokemon, move: Move): number => { + 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 - for (const tm of moveHistory) { - if ( - !(tm.move === move.id || comboMoves.includes(tm.move)) - || (resetOnFail && tm.result !== MoveResult.SUCCESS) - ) { + while ( + ( + (turnMove = moveHistory.shift())?.move === move.id + || (comboMoves.length && comboMoves.includes(turnMove?.move ?? Moves.NONE)) + ) + && (!resetOnFail || turnMove?.result === MoveResult.SUCCESS) + ) { + if (count < (limit - 1)) { + count++; + } else if (resetOnLimit) { + count = 0; + } else { break; } - - if (count < limit - 1) { - count++; - continue; - } - - if (resetOnLimit) { - count = 0; - } - - break; } 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 - * 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 */ export class OpponentHighHpPowerAttr extends VariablePowerAttr { @@ -4333,7 +4317,6 @@ const hasStockpileStacksCondition: MoveConditionFunc = (user) => { return !!hasStockpilingTag && hasStockpilingTag.stockpiledCount > 0; }; - /** * 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. @@ -5434,17 +5417,11 @@ export class FrenzyAttr extends MoveEffectAttr { } // TODO: Disable if used via dancer - // 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, - // add 1-2 extra instances of the move to the move queue. - // If frenzy is already in effect, tick down the tag. - 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 }); - } + if (!user.getTag(BattlerTagType.FRENZY) && !user.getMoveQueue().length) { + const turnCount = user.randSeedIntRange(1, 2); + new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true })); user.addTag(BattlerTagType.FRENZY, turnCount, move.id, user.id); } else { applyMoveAttrs(AddBattlerTagAttr, user, target, move, args); @@ -5817,24 +5794,23 @@ export class ProtectAttr extends AddBattlerTagAttr { 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 { return ((user, target, move): boolean => { - const lastMoves = user.getLastXMoves(-1) + let timesUsed = 0; + const moveHistory = user.getLastXMoves(); + let turnMove: TurnMove | undefined; - let threshold = 1; - for (const tm of lastMoves) { - if (!allMoves[tm.move].hasAttr(ProtectAttr) || tm.result !== MoveResult.SUCCESS) { + while (moveHistory.length) { + turnMove = moveHistory.shift(); + if (!allMoves[turnMove?.move ?? Moves.NONE].hasAttr(ProtectAttr) || turnMove?.result !== MoveResult.SUCCESS) { break; } - threshold *= 3; + timesUsed++; } - - return threshold === 1 || user.randSeedInt(threshold) === 0; + if (timesUsed) { + return !user.randSeedInt(Math.pow(3, timesUsed)); + } + return true; }); } } @@ -7334,14 +7310,13 @@ export class SketchAttr extends MoveEffectAttr { constructor() { super(true); } - /** - * User copies the opponent's last used move, if possible. - * @param user - The {@linkcode Pokemon} using the move - * @param target - The {@linkcode Pokemon} being targeted by the move - * @param move - The {@linkcoed Move} being used - * @param args - Unused - * @returns Whether a move was successfully learnt + * User copies the opponent's last used move, if possible + * @param {Pokemon} user Pokemon that used the move and will replace Sketch with the copied move + * @param {Pokemon} target Pokemon that the user wants to copy a move from + * @param {Move} move Move being used + * @param {any[]} args Unused + * @returns {boolean} true if the function succeeds, otherwise false */ 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. * If applied to multiple targets, they move in speed order after all other moves. + * @extends MoveEffectAttr */ export class ForceLastAttr extends MoveEffectAttr { /** @@ -7927,7 +7903,7 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean let slower: boolean; // quashed pokemon still have speed ties if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) { - slower = !target.randSeedInt(2); + slower = target.randSeedInt(2) === 0; } else { slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f2d00732ccf..e5877fee381 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -642,7 +642,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Checks if a pokemon is fainted (ie: its `hp <= 0`). * 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 */ 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. * * 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 applyModifiersToOverride - Whether to apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}. - * Does nothing if {@linkcode thresholdOverride} is not set. + * @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 If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise */ public trySetShinySeed( @@ -3812,7 +3811,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ui => ui instanceof BattleInfo && (ui as BattleInfo) instanceof PlayerBattleInfo === this.isPlayer(), - )[0] as Phaser.GameObjects.GameObject | undefined; + ) + .find(() => true); if (!otherBattleInfo || !this.getFieldIndex()) { globalScene.fieldUI.sendToBack(this.battleInfo); 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. * The retrieved move entries are sorted in order from NEWEST to OLDEST. - * @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()}). + * @param moveCount The number of move entries to retrieve. + * If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}). * Default is `1`. * @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) { sleepTurnsRemaining = new NumberHolder(this.randSeedIntRange(2, 4)); + this.setFrameRate(4); - // If the user is invulnerable, remove their invulnerability when they fall asleep - // and remove the upcoming attack from the move queue. - const tag = [ + // If the user is invulnerable, lets remove their invulnerability when they fall asleep + const invulnerableTags = [ BattlerTagType.UNDERGROUND, BattlerTagType.UNDERWATER, BattlerTagType.HIDDEN, BattlerTagType.FLYING, - ].find(t => this.getTag(t)); + ]; + + const tag = invulnerableTags.find(t => this.getTag(t)); + if (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}). - * @param count - Amount of PP to consume; default `1` + * Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp} + * @param count Amount of PP to use */ - usePp(count = 1) { + usePp(count: number = 1) { this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp()); } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index c1be34d7827..a4469b79446 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -135,11 +135,11 @@ export class MoveEffectPhase extends PokemonPhase { * Compute targets and the results of hit checks of the invoked move against all targets, * organized by battler index. * - * **This is *not* a pure function** and has the following side effects: - * - Sets `this.hitChecks` to the results of the hit checks against each target - * - Sets success/failure of `this.moveHistoryEntry` based on the hit check results - * - Sets `user.turnData.hitCount` and `user.turnData.hitsLeft` to 1 if the move - * was unsuccessful against all targets (effectively canceling it) + * **This is *not* a pure function**; it has the following side effects + * - `this.hitChecks` - The results of the hit checks against each target + * - `this.moveHistoryEntry` - Sets success or failure based on the hit check results + * - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the + * move was unsuccessful against all targets * * @returns The targets of the invoked move * @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 - * @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 { for (const [i, target] of targets.entries()) { @@ -229,7 +229,6 @@ export class MoveEffectPhase extends PokemonPhase { case HitCheckResult.NO_EFFECT_NO_MESSAGE: case HitCheckResult.PROTECTED: case HitCheckResult.TARGET_NOT_ON_FIELD: - // Apply effects for ineffective moves (e.g. High Jump Kick crash dmg) applyMoveAttrs(NoEffectAttr, user, target, this.move); break; case HitCheckResult.MISS: @@ -643,19 +642,22 @@ export class MoveEffectPhase extends PokemonPhase { if (!user) { return false; } - - switch (true) { - // No Guard - case user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr): - // Toxic as poison type - case this.move.hasAttr(ToxicAccuracyAttr) && user.isOfType(PokemonType.POISON): - // Lock On/Mind Reader - case !!user.getTag(BattlerTagType.IGNORE_ACCURACY): - // Spikes and company - case isFieldTargeted(this.move): - return true; + if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { + return true; + } + if (this.move.hasAttr(ToxicAccuracyAttr) && user.isOfType(PokemonType.POISON)) { + return true; + } + // TODO: Fix lock on / mind reader check. + if ( + user.getTag(BattlerTagType.IGNORE_ACCURACY) && + (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1 + ) { + return true; + } + if (isFieldTargeted(this.move)) { + return true; } - return false; } /** diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 08bdf47cdfe..452bfa3bb6b 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -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. * 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[] { const playerField = globalScene.getPlayerField().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 - // This is seeded with the current turn to prevent an inconsistency with variable turn order - // based on how long since you last reloaded + // We shuffle the list before sorting so speed ties produce random results 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( () => { orderedTargets = randSeedShuffle(orderedTargets); @@ -45,11 +45,11 @@ export class TurnStartPhase extends FieldPhase { globalScene.waveSeed, ); - // Check for Trick Room and reverse sort order if active. - // Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd. + // Next, a check for Trick Room is applied to determine sort order. const speedReversed = new BooleanHolder(false); globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); + // Adjust the sort function based on whether Trick Room is active. orderedTargets.sort((a: Pokemon, b: Pokemon) => { const aSpeed = a?.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, - // check for differences in battlerBypassSpeed and returns the result. + // If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result. if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { return battlerBypassSpeed[a].value ? -1 : 1; } diff --git a/test/abilities/wimp_out.test.ts b/test/abilities/wimp_out.test.ts index 59ed56022cf..f558efdb103 100644 --- a/test/abilities/wimp_out.test.ts +++ b/test/abilities/wimp_out.test.ts @@ -44,6 +44,7 @@ describe("Abilities - Wimp Out", () => { function confirmSwitch(): void { const [pokemon1, pokemon2] = game.scene.getPlayerParty(); + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD); @@ -55,34 +56,17 @@ describe("Abilities - Wimp Out", () => { function confirmNoSwitch(): void { const [pokemon1, pokemon2] = game.scene.getPlayerParty(); + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD); + expect(pokemon1.species.speciesId).toBe(Species.WIMPOD); expect(pokemon1.isFainted()).toBe(false); 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 () => { - 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 () => { + it("triggers regenerator passive single time when switching out with wimp out", async () => { game.override.passiveAbility(Abilities.REGENERATOR).startingLevel(5).enemyLevel(100); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -96,7 +80,7 @@ describe("Abilities - Wimp Out", () => { confirmSwitch(); }); - it("should cause wild pokemon to flee", async () => { + it("It makes wild pokemon flee if triggered", async () => { game.override.enemyAbility(Abilities.WIMP_OUT); await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]); @@ -106,11 +90,12 @@ describe("Abilities - Wimp Out", () => { game.move.select(Moves.FALSE_SWIPE); await game.phaseInterceptor.to("BerryPhase"); - expect(enemyPokemon.visible).toBe(false); - expect(enemyPokemon.switchOutStatus).toBe(true); + const isVisible = enemyPokemon.visible; + 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]); const wimpod = game.scene.getPlayerPokemon()!; wimpod.hp = 5; @@ -122,7 +107,7 @@ describe("Abilities - Wimp Out", () => { 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); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -137,7 +122,7 @@ describe("Abilities - Wimp Out", () => { 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]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -151,7 +136,7 @@ describe("Abilities - Wimp Out", () => { 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]); await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]); const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id; @@ -160,7 +145,7 @@ describe("Abilities - Wimp Out", () => { 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]); 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); }); - it("should trigger from recoil damage", async () => { + it("triggers when recoil damage is taken", async () => { game.override.moveset([Moves.HEAD_SMASH]).enemyMoveset([Moves.SPLASH]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -188,7 +173,7 @@ describe("Abilities - Wimp Out", () => { 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]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -201,19 +186,7 @@ describe("Abilities - Wimp Out", () => { confirmNoSwitch(); }); - it("should not trigger from Sheer Force-boosted moves", 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 () => { + it("Does not trigger when neutralized", async () => { game.override.enemyAbility(Abilities.NEUTRALIZING_GAS).startingLevel(5); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -250,140 +223,106 @@ describe("Abilities - Wimp Out", () => { }, ); - // TODO: Condense into it.eaches - describe("Post Turn Damage Checks - ", () => { - beforeEach(() => { - game.override.enemyMoveset(Moves.SPLASH); - }); + it("Wimp Out will activate due to weather damage", async () => { + game.override.weather(WeatherType.HAIL).enemyMoveset([Moves.SPLASH]); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - it("Wimp Out will activate due to weather damage", async () => { - game.override.weather(WeatherType.HAIL); - 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.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); - game.move.select(Moves.SPLASH); - 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(); - }); + 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.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); game.override @@ -392,7 +331,6 @@ describe("Abilities - Wimp Out", () => { .weather(WeatherType.HAIL) .statusEffect(StatusEffect.POISON); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - game.scene.getPlayerPokemon()!.hp *= 0.51; game.move.select(Moves.SPLASH); @@ -403,19 +341,6 @@ describe("Abilities - Wimp Out", () => { 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 () => { game.override.battleStyle("double").enemyAbility(Abilities.WIMP_OUT).enemyMoveset([Moves.SPLASH]).enemyLevel(1); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -436,7 +361,59 @@ describe("Abilities - Wimp Out", () => { 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); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -467,7 +444,6 @@ describe("Abilities - Wimp Out", () => { expect(enemyPokemon.turnData.hitCount).toBe(2); confirmSwitch(); }); - it("triggers after last hit of Parental Bond", async () => { game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.PARENTAL_BOND); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);