Wimp Out changes

This commit is contained in:
Bertie690 2025-05-14 19:00:26 -04:00
parent 37ad950093
commit 1f7c67423b
9 changed files with 240 additions and 286 deletions

View File

@ -21,6 +21,7 @@ import {
NeutralDamageAgainstFlyingTypeMultiplierAttr,
FixedDamageAttr,
type MoveAttr,
ForceSwitchOutAttr,
} from "#app/data/moves/move";
import { ArenaTagSide } from "#app/data/arena-tag";
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
@ -3662,7 +3663,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
/**
* Condition function to applied to abilities related to Sheer Force.
* Checks if last move used against target was affected by a Sheer Force user and:
* Disables: Color Change, Pickpocket, Berserk, Anger Shell
* Disables: Color Change, Pickpocket, Berserk, Anger Shell, Wimp Out and Emergency Exit.
* @returns An {@linkcode AbAttrCondition} to disable the ability under the proper conditions.
*/
function getSheerForceHitDisableAbCondition(): AbAttrCondition {
@ -5547,12 +5548,12 @@ function applySingleAbAttrs<TAttr extends AbAttr>(
* Shell Bell's modifier (if any).
*
* @param pokemon - The Pokémon whose Shell Bell recovery is being calculated.
* @returns The amount of health recovered by Shell Bell.
* @returns The amount of health recovered by Shell Bell, or `0` if none are present.
*/
function calculateShellBellRecovery(pokemon: Pokemon): number {
const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier);
if (shellBellModifier) {
return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount;
return toDmgValue(pokemon.turnData.lastMoveDamageDealt / 8) * shellBellModifier.stackCount;
}
return 0;
}
@ -5565,20 +5566,19 @@ export class PostDamageAbAttr extends AbAttr {
public canApplyPostDamage(
pokemon: Pokemon,
damage: number,
passive: boolean,
simulated: boolean,
args: any[],
source?: Pokemon): boolean {
source: Pokemon | undefined,
args: any[]
): boolean {
return true;
}
public applyPostDamage(
pokemon: Pokemon,
damage: number,
passive: boolean,
simulated: boolean,
args: any[],
source?: Pokemon,
source: Pokemon | undefined,
args: any[]
): void {}
}
@ -5587,75 +5587,65 @@ export class PostDamageAbAttr extends AbAttr {
* This attribute checks various conditions related to the damage received, the moves used by the Pokémon
* and its opponents, and determines whether a forced switch-out should occur.
*
* Used by Wimp Out and Emergency Exit
* Used for Wimp Out and Emergency Exit
*
* @extends PostDamageAbAttr
* @see {@linkcode applyPostDamage}
*/
export class PostDamageForceSwitchAbAttr extends ForceSwitch(PostDamageAbAttr) {
private hpRatio: number;
constructor(selfSwitch = true, switchType: NormalSwitchType = SwitchType.SWITCH, hpRatio = 0.5) {
constructor(switchType: NormalSwitchType = SwitchType.SWITCH, hpRatio = 0.5) {
super();
this.selfSwitch = selfSwitch;
this.selfSwitch = false; // TODO: change if any abilities get damage
this.switchType = switchType;
this.hpRatio = hpRatio;
}
// TODO: Refactor to use more early returns
/**
* Check to see if the user should be switched out after taking damage.
* @param pokemon - The {@linkcode Pokemon} with this ability; will be switched out if conditions are met.
* @param damage - The amount of damage dealt by the triggering damage instance.
* @param _simulated - unused
* @param source - The {@linkcode Pokemon} having damaged the user with an attack, or `undefined`
* if the damage source was indirect.
* @param _args - unused
* @returns Whether this pokemon should be switched out upon move conclusion.
*/
public override canApplyPostDamage(
pokemon: Pokemon,
damage: number,
passive: boolean,
simulated: boolean,
args: any[],
source?: Pokemon): boolean {
const moveHistory = pokemon.getMoveHistory();
_simulated: boolean,
source: Pokemon | undefined,
_args: any[],
): boolean {
const userLastMove = pokemon.getLastXMoves()[0];
// Will not activate when the Pokémon's HP is lowered by cutting its own HP
const fordbiddenAttackingMoves = [ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ];
if (moveHistory.length > 0) {
const lastMoveUsed = moveHistory[moveHistory.length - 1];
if (fordbiddenAttackingMoves.includes(lastMoveUsed.move)) {
const forbiddenAttackingMoves = new Set<Moves>([ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ]);
if (!isNullOrUndefined(userLastMove) && forbiddenAttackingMoves.has(userLastMove.move)) {
return false;
}
}
// Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.
const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ];
if (source) {
const enemyMoveHistory = source.getMoveHistory();
if (enemyMoveHistory.length > 0) {
const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1];
// Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop.
if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move) || enemyLastMoveUsed.move === Moves.SKY_DROP && enemyLastMoveUsed.result === MoveResult.OTHER) {
// Skip last move checks if no enemy move
const lastMove = source?.getLastXMoves()[0]
if (
lastMove &&
// Will not activate for forced switch moves (triggers before wimp out activates)
(allMoves[lastMove.move].hasAttr(ForceSwitchOutAttr)
// Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop
// TODO: Make this check the user's tags rather than the last move used by the target - we could be lifted by another pokemon
|| (lastMove.move === Moves.SKY_DROP && lastMove.result === MoveResult.OTHER))
) {
return false;
// Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force.
// TODO: Make this use the sheer force disable condition
} else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && source.hasAbility(Abilities.SHEER_FORCE)) {
return false;
// Activate only after the last hit of multistrike moves
} else if (source.turnData.hitsLeft > 1) {
return false;
}
if (source.turnData.hitCount > 1) {
damage = pokemon.turnData.damageTaken;
}
}
}
if (pokemon.hp + damage >= pokemon.getMaxHp() * this.hpRatio) {
const shellBellHeal = calculateShellBellRecovery(pokemon);
if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) {
for (const opponent of pokemon.getOpponents()) {
if (!this.canSwitchOut(pokemon, opponent)) {
// Check for HP percents - don't switch if the move didn't knock us below our switch threshold
// (either because we were below it to begin with or are still above it after the hit).
const hpNeededToSwitch = pokemon.getMaxHp() * this.hpRatio;
if (pokemon.hp + damage < hpNeededToSwitch || pokemon.hp >= hpNeededToSwitch) {
return false;
}
}
return true;
}
}
return false;
return this.canSwitchOut(pokemon, oppponent)
}
/**
@ -5663,7 +5653,7 @@ export class PostDamageForceSwitchAbAttr extends ForceSwitch(PostDamageAbAttr) {
*
* @param pokemon The Pokémon that took damage.
*/
public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): void {
public override applyPostDamage(pokemon: Pokemon, _damage: number, _simulated: boolean, _source: Pokemon | undefined, args: any[]): void {
this.doSwitch(pokemon);
}
}
@ -5837,16 +5827,15 @@ export function applyPostDamageAbAttrs(
attrType: Constructor<PostDamageAbAttr>,
pokemon: Pokemon,
damage: number,
passive: boolean,
simulated = false,
args: any[],
source?: Pokemon,
source: Pokemon | undefined = undefined,
...args: any[]
): void {
applyAbAttrsInternal<PostDamageAbAttr>(
attrType,
pokemon,
(attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source),
(attr, passive) => attr.canApplyPostDamage(pokemon, damage, passive, simulated, args, source),
(attr, passive) => attr.applyPostDamage(pokemon, damage, simulated, source, args),
(attr, passive) => attr.canApplyPostDamage(pokemon, damage, simulated, source, args),
args,
);
}
@ -6942,9 +6931,11 @@ export function initAbilities() {
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(Abilities.WIMP_OUT, 7)
.attr(PostDamageForceSwitchAbAttr)
.condition(getSheerForceHitDisableAbCondition())
.edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode
new Ability(Abilities.EMERGENCY_EXIT, 7)
.attr(PostDamageForceSwitchAbAttr)
.condition(getSheerForceHitDisableAbCondition())
.edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode
new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),

View File

@ -1551,6 +1551,7 @@ export class MatchHpAttr extends FixedDamageAttr {
type MoveFilter = (move: Move) => boolean;
// TODO: fix this to check the last direct damage instance taken
export class CounterDamageAttr extends FixedDamageAttr {
private moveFilter: MoveFilter;
private multiplier: number;
@ -1664,8 +1665,8 @@ export class RecoilAttr extends MoveEffectAttr {
return false;
}
const damageValue = (!this.useHp ? user.turnData.totalDamageDealt : user.getMaxHp()) * this.damageRatio;
const minValue = user.turnData.totalDamageDealt ? 1 : 0;
const damageValue = (!this.useHp ? user.turnData.lastMoveDamageDealt : user.getMaxHp()) * this.damageRatio;
const minValue = user.turnData.lastMoveDamageDealt ? 1 : 0;
const recoilDamage = toDmgValue(damageValue, minValue);
if (!recoilDamage) {
return false;

View File

@ -342,6 +342,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public luck: number;
public pauseEvolutions: boolean;
public pokerus: boolean;
// TODO: Document these
public switchOutStatus = false;
public evoCounter: number;
public teraType: PokemonType;
@ -4748,13 +4749,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Given the damage, adds a new DamagePhase and update HP values, etc.
*
* Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly
* @param damage integer - passed to damage()
* @param result an enum if it's super effective, not very, etc.
* @param isCritical boolean if move is a critical hit
* @param ignoreSegments boolean, passed to damage() and not used currently
* @param preventEndure boolean, ignore endure properties of pokemon, passed to damage()
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
* @returns integer of damage done
* @param damage - Amount of damage to deal
* @param result - The {@linkcode HitResult} of the damage instance; default `HitResult.EFFECTIVE`
* @param isCritical - Whether the move being used (if any) was a critical hit; default `false`
* @param ignoreSegments - Whether to ignore boss segments; default `false` and currently unused
* @param preventEndure - Whether to ignore {@linkcode Moves.ENDURE} and similar effects when applying damage; default `false`
* @param ignoreFaintPhase - Whether to ignore adding a faint phase if the damage causes the target to faint; default `false`
* @returns The amount of damage actually dealt.
* @remarks
* This will not trigger "on damage" effects for direct damage moves, which instead occurs at the end of `MoveEffectPhase`.
*/
damageAndUpdate(damage: number,
{
@ -4762,53 +4765,52 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
isCritical = false,
ignoreSegments = false,
ignoreFaintPhase = false,
source = undefined,
}:
{
result?: DamageResult,
isCritical?: boolean,
ignoreSegments?: boolean,
ignoreFaintPhase?: boolean,
source?: Pokemon,
} = {}
): number {
const isIndirectDamage = [ HitResult.INDIRECT, HitResult.INDIRECT_KO ].includes(result);
const damagePhase = new DamageAnimPhase(
this.getBattlerIndex(),
damage,
result as DamageResult,
result,
isCritical
);
globalScene.unshiftPhase(damagePhase);
if (this.switchOutStatus && source) {
// TODO: Review if wimp out battle skip actually needs this anymore
if (this.switchOutStatus) {
damage = 0;
}
damage = this.damage(
damage,
ignoreSegments,
isIndirectDamage,
ignoreFaintPhase,
);
// Ensure the battle-info bar's HP is updated, though only if the battle info is visible
// TODO: When battle-info UI is refactored, make this only update the HP bar
if (this.battleInfo.visible) {
this.updateInfo();
}
// Damage amount may have changed, but needed to be queued before calling damage function
damagePhase.updateAmount(damage);
/**
* Run PostDamageAbAttr from any source of damage that is not from a multi-hit
* Multi-hits are handled in move-effect-phase.ts for PostDamageAbAttr
*/
if (!source || source.turnData.hitCount <= 1) {
// Trigger PostDamageAbAttr (ie wimp out) for indirect damage only.
if (isIndirectDamage) {
applyPostDamageAbAttrs(
PostDamageAbAttr,
this,
damage,
this.hasPassive(),
false,
[],
source,
undefined
);
}
return damage;
@ -7957,8 +7959,13 @@ export class PokemonTurnData {
* - `0` = Move is finished
*/
public hitsLeft = -1;
public totalDamageDealt = 0;
/**
* The amount of damage dealt by this Pokemon's last attack.
* Reset upon successfully using a move and used to enable internal tracking of damage amounts.
*/
public lastMoveDamageDealt = 0;
public singleHitDamageDealt = 0;
// TODO: Rework this into "damage taken last" counter for metal burst and co.
public damageTaken = 0;
public attacksReceived: AttackMoveResult[] = [];
public order: number;
@ -8007,7 +8014,7 @@ export enum HitResult {
FAIL,
MISS,
INDIRECT,
IMMUNE,
IMMUNE, // TODO: Why is this used exclusively for sheer cold?
CONFUSION,
INDIRECT_KO,
}

View File

@ -1783,12 +1783,12 @@ export class HitHealModifier extends PokemonHeldItemModifier {
* @returns `true` if the {@linkcode Pokemon} was healed
*/
override apply(pokemon: Pokemon): boolean {
if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) {
if (pokemon.turnData.lastMoveDamageDealt && !pokemon.isFullHp()) {
// TODO: this shouldn't be undefined AFAIK
globalScene.unshiftPhase(
new PokemonHealPhase(
pokemon.getBattlerIndex(),
toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount,
toDmgValue(pokemon.turnData.lastMoveDamageDealt / 8) * this.stackCount,
i18next.t("modifier:hitHealApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
typeName: this.type.name,

View File

@ -10,7 +10,6 @@ import {
IgnoreMoveEffectsAbAttr,
MaxMultiHitAbAttr,
PostAttackAbAttr,
PostDamageAbAttr,
PostDefendAbAttr,
ReflectStatusMoveAbAttr,
} from "#app/data/abilities/ability";
@ -48,7 +47,7 @@ import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/MoveCategory";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { PokemonType } from "#enums/pokemon-type";
import { DamageResult, PokemonMove, type TurnMove } from "#app/field/pokemon";
import { type DamageResult, PokemonMove, type TurnMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { HitResult, MoveResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
@ -72,7 +71,7 @@ import { ShowAbilityPhase } from "./show-ability-phase";
import { MovePhase } from "./move-phase";
import { MoveEndPhase } from "./move-end-phase";
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
import { TypeDamageMultiplier } from "#app/data/type";
import type { TypeDamageMultiplier } from "#app/data/type";
import { HitCheckResult } from "#enums/hit-check-result";
import type Move from "#app/data/moves/move";
import { isFieldTargeted } from "#app/data/moves/move-utils";
@ -101,6 +100,9 @@ export class MoveEffectPhase extends PokemonPhase {
/** Phases queued during moves */
private queuedPhases: Phase[] = [];
/** The amount of direct attack damage taken by each of this Phase's targets. */
private targetDamageTaken: number[] = [];
/**
* @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce
* @param virtual Indicates that the move is a virtual move (i.e. called by metronome)
@ -123,6 +125,7 @@ export class MoveEffectPhase extends PokemonPhase {
this.targets = targets;
this.hitChecks = Array(this.targets.length).fill([HitCheckResult.PENDING, 0]);
this.targetDamageTaken = Array(this.targets.length).fill(0);
}
/**
@ -785,11 +788,6 @@ export class MoveEffectPhase extends PokemonPhase {
}
if (this.lastHit) {
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
// Multi-hit check for Wimp Out/Emergency Exit
if (user.turnData.hitCount > 1) {
applyPostDamageAbAttrs(PostDamageAbAttr, target, 0, target.hasPassive(), false, [], user);
}
}
}
@ -821,6 +819,7 @@ export class MoveEffectPhase extends PokemonPhase {
isCritical,
});
// Apply and/or remove type boosting tags (Flash Fire, Charge, etc.)
const typeBoost = user.findTag(
t => t instanceof TypeBoostTag && t.boostedType === user.getMoveType(this.move),
) as TypeBoostTag;
@ -828,18 +827,17 @@ export class MoveEffectPhase extends PokemonPhase {
user.removeTag(typeBoost.tagType);
}
const isOneHitKo = result === HitResult.ONE_HIT_KO;
if (!dmg) {
if (dmg === 0) {
return result;
}
const isOneHitKo = result === HitResult.ONE_HIT_KO;
target.lapseTags(BattlerTagLapseType.HIT);
const substitute = target.getTag(SubstituteTag);
const isBlockedBySubstitute = substitute && this.move.hitsSubstitute(user, target);
const substituteTag = target.getTag(SubstituteTag);
const isBlockedBySubstitute = substituteTag && this.move.hitsSubstitute(user, target);
if (isBlockedBySubstitute) {
substitute.hp -= dmg;
substituteTag.hp -= dmg;
} else if (!target.isPlayer() && dmg >= target.hp) {
globalScene.applyModifiers(EnemyEndureChanceModifier, false, target);
}
@ -851,7 +849,6 @@ export class MoveEffectPhase extends PokemonPhase {
ignoreFaintPhase: true,
ignoreSegments: isOneHitKo,
isCritical,
source: user,
});
if (isCritical) {
@ -865,14 +862,13 @@ export class MoveEffectPhase extends PokemonPhase {
if (user.isPlayer()) {
globalScene.validateAchvs(DamageAchv, new NumberHolder(damage));
if (damage > globalScene.gameData.gameStats.highestDamage) {
globalScene.gameData.gameStats.highestDamage = damage;
}
globalScene.gameData.gameStats.highestDamage = Math.max(damage, globalScene.gameData.gameStats.highestDamage);
}
user.turnData.totalDamageDealt += damage;
user.turnData.lastMoveDamageDealt += damage;
user.turnData.singleHitDamageDealt = damage;
target.battleData.hitCount++;
// TODO: this might be incorrect for counter moves
target.turnData.damageTaken += damage;
target.turnData.attacksReceived.unshift({

View File

@ -49,9 +49,10 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
}
if (damage.value) {
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
// TODO: why don't we call `damageAndUpdate` here?
globalScene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
pokemon.updateInfo();
applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []);
applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value);
}
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(false, () => this.end());
} else {

View File

@ -37,7 +37,7 @@ describe("Abilities - Wimp Out", () => {
.enemyPassiveAbility(Abilities.NO_GUARD)
.startingLevel(90)
.enemyLevel(70)
.moveset([Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE])
.moveset([Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE, Moves.THUNDER_PUNCH])
.enemyMoveset(Moves.FALSE_SWIPE)
.disableCrits();
});
@ -66,7 +66,40 @@ describe("Abilities - Wimp Out", () => {
expect(pokemon1.getHpRatio()).toBeLessThan(0.5);
}
it("triggers regenerator passive single time when switching out with wimp out", async () => {
it("should switch the user out when falling below half HP, canceling its subsequent moves", async () => {
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp *= 0.52;
game.move.select(Moves.THUNDER_PUNCH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
// Wimpod switched out after taking a hit, canceling its upcoming MovePhase before it could attack
confirmSwitch();
expect(game.scene.getEnemyPokemon()!.getInverseHp()).toBe(0);
expect(game.phaseInterceptor.log.reduce((count, phase) => count + (phase === "MoveEffectPhase" ? 1 : 0), 0)).toBe(
1,
);
});
it("should not trigger if user faints from damage", async () => {
game.override.enemyMoveset(Moves.BRAVE_BIRD).enemyLevel(1000);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp *= 0.52;
game.move.select(Moves.THUNDER_PUNCH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(wimpod.isFainted()).toBe(true);
confirmNoSwitch();
});
it("should trigger regenerator passive when switching out", async () => {
game.override.passiveAbility(Abilities.REGENERATOR).startingLevel(5).enemyLevel(100);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -80,7 +113,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("It makes wild pokemon flee if triggered", async () => {
it("should cause wild pokemon to flee when triggered", async () => {
game.override.enemyAbility(Abilities.WIMP_OUT);
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
@ -95,7 +128,7 @@ describe("Abilities - Wimp Out", () => {
expect(!isVisible && hasFled).toBe(true);
});
it("Does not trigger when HP already below half", async () => {
it("should not trigger when HP already below half", async () => {
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp = 5;
@ -107,7 +140,7 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch();
});
it("Trapping moves do not prevent Wimp Out from activating.", async () => {
it("should bypass trapping moves", async () => {
game.override.enemyMoveset([Moves.SPIRIT_SHACKLE]).startingLevel(53).enemyLevel(45);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -122,7 +155,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
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 () => {
it("should block U-turn or Volt Switch on activation", async () => {
game.override.startingLevel(95).enemyMoveset([Moves.U_TURN]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -136,7 +169,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
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 () => {
it("should not block U-turn or Volt Switch if not activated", 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;
@ -145,7 +178,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("should not activate from Dragon Tail and Circle Throw", async () => {
game.override.startingLevel(69).enemyMoveset([Moves.DRAGON_TAIL]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -162,81 +195,41 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD);
});
it("triggers when recoil damage is taken", async () => {
game.override.moveset([Moves.HEAD_SMASH]).enemyMoveset([Moves.SPLASH]);
it.each<{ type: string; enemyMove?: Moves; enemyAbility?: Abilities }>([
{ type: "weather", enemyMove: Moves.HAIL },
{ type: "status", enemyMove: Moves.TOXIC },
{ type: "Curse", enemyMove: Moves.CURSE },
{ type: "Salt Cure", enemyMove: Moves.SALT_CURE },
{ type: "partial trapping moves", enemyMove: Moves.WHIRLPOOL },
{ type: "Leech Seed", enemyMove: Moves.LEECH_SEED },
{ type: "Nightmare", enemyMove: Moves.NIGHTMARE },
{ type: "Aftermath", enemyAbility: Abilities.AFTERMATH },
{ type: "Bad Dreams", enemyAbility: Abilities.BAD_DREAMS },
])(
"should activate from damage caused by $name",
async ({ enemyMove = Moves.SPLASH, enemyAbility = Abilities.BALL_FETCH }) => {
game.override
.passiveAbility(Abilities.COMATOSE)
.enemySpecies(Species.GASTLY)
.enemyMoveset(enemyMove)
.enemyAbility(enemyAbility)
.enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.move.select(Moves.HEAD_SMASH);
const wimpod = game.scene.getPlayerPokemon()!;
expect(wimpod).toBeDefined();
wimpod.hp *= 0.55;
game.move.select(Moves.THUNDER_PUNCH);
game.doSelectPartyPokemon(1);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
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]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp *= 0.52;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("Does not trigger when neutralized", async () => {
game.override.enemyAbility(Abilities.NEUTRALIZING_GAS).startingLevel(5);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
// TODO: Enable when this behavior is fixed (currently Shell Bell won't activate if Wimp Out activates because
// the pokemon is removed from the field before the Shell Bell modifier is applied, so it can't see the
// damage dealt and doesn't heal the pokemon)
it.todo(
"If it falls below half and recovers back above half from a Shell Bell, Wimp Out will activate even after the Shell Bell recovery",
async () => {
game.override
.moveset([Moves.DOUBLE_EDGE])
.enemyMoveset([Moves.SPLASH])
.startingHeldItems([{ name: "SHELL_BELL", count: 4 }]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.damageAndUpdate(toDmgValue(wimpod.getMaxHp() * 0.4));
game.move.select(Moves.DOUBLE_EDGE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerParty()[1]).toBe(wimpod);
expect(wimpod.hp).toBeGreaterThan(toDmgValue(wimpod.getMaxHp() / 2));
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT);
},
);
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]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
it("Does not trigger when enemy has sheer force", async () => {
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]);
@ -248,81 +241,79 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch();
});
it("Wimp Out will activate due to post turn status damage", async () => {
game.override.statusEffect(StatusEffect.POISON).enemyMoveset([Moves.SPLASH]);
it("should trigger from recoil damage", async () => {
game.override.moveset(Moves.HEAD_SMASH).enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.move.select(Moves.HEAD_SMASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
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]);
it("should trigger from Flame Burst ally damage in doubles", async () => {
game.override.battleStyle("double").enemyMoveset([Moves.FLAME_BURST, Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.ZYGARDE, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
const wimpod = game.scene.getPlayerPokemon()!;
expect(wimpod).toBeDefined();
wimpod.hp *= 0.55;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER);
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
await game.forceEnemyMove(Moves.FLAME_BURST, BattlerIndex.PLAYER_2);
await game.forceEnemyMove(Moves.SPLASH);
game.doSelectPartyPokemon(2);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
});
it("Wimp Out will activate due to leech seed", async () => {
game.override.enemyMoveset([Moves.LEECH_SEED]);
it("should 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]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp *= 0.52;
confirmSwitch();
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("Wimp Out will activate due to curse damage", async () => {
game.override.enemySpecies(Species.DUSKNOIR).enemyMoveset([Moves.CURSE]);
it("should not trigger when neutralized", async () => {
game.override.enemyAbility(Abilities.NEUTRALIZING_GAS).startingLevel(5);
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();
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
confirmNoSwitch();
});
it("Wimp Out will activate due to salt cure damage", async () => {
game.override.enemySpecies(Species.NACLI).enemyMoveset([Moves.SALT_CURE]).enemyLevel(1);
it("should disregard Shell Bell recovery while still activating it before switching", async () => {
game.override
.moveset([Moves.DOUBLE_EDGE])
.enemyMoveset([Moves.SPLASH])
.startingHeldItems([{ name: "SHELL_BELL", count: 4 }]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.7;
game.move.select(Moves.SPLASH);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp *= 0.51;
game.move.select(Moves.DOUBLE_EDGE);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
expect(game.scene.getPlayerParty()[1]).toBe(wimpod);
expect(wimpod.hp).toBeGreaterThan(toDmgValue(wimpod.getMaxHp() / 2));
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT);
});
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 () => {
it("should not switch if Magic Guard prevents damage", 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
@ -341,7 +332,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD);
});
it("Wimp Out activating should not cancel a double battle", async () => {
it("should not cancel a double battle on activation", async () => {
game.override.battleStyle("double").enemyAbility(Abilities.WIMP_OUT).enemyMoveset([Moves.SPLASH]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const enemyLeadPokemon = game.scene.getEnemyParty()[0];
@ -361,24 +352,7 @@ describe("Abilities - Wimp Out", () => {
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
});
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 () => {
it("should activate from entry hazard damage", 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);
@ -388,18 +362,6 @@ describe("Abilities - Wimp Out", () => {
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]);
@ -413,7 +375,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("triggers after last hit of multi hit move", async () => {
it("triggers after last hit of multi hit moves", async () => {
game.override.enemyMoveset(Moves.BULLET_SEED).enemyAbility(Abilities.SKILL_LINK);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -429,7 +391,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("triggers after last hit of multi hit move (multi lens)", async () => {
it("triggers after last hit of multi hit moves from multi lens", async () => {
game.override.enemyMoveset(Moves.TACKLE).enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -444,6 +406,7 @@ 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]);
@ -461,9 +424,7 @@ describe("Abilities - Wimp Out", () => {
});
// TODO: This interaction is not implemented yet
it.todo(
"Wimp Out will not activate if the Pokémon's HP falls below half due to hurting itself in confusion",
async () => {
it.todo("should not activate if the Pokémon's HP falls below half due to hurting itself in confusion", async () => {
game.override.moveset([Moves.SWORDS_DANCE]).enemyMoveset([Moves.SWAGGER]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const playerPokemon = game.scene.getPlayerPokemon()!;
@ -479,8 +440,7 @@ describe("Abilities - Wimp Out", () => {
}
confirmNoSwitch();
},
);
});
it("should not activate on wave X0 bosses", async () => {
game.override.enemyAbility(Abilities.WIMP_OUT).startingLevel(5850).startingWave(10);
@ -499,7 +459,7 @@ describe("Abilities - Wimp Out", () => {
expect(isVisible && !hasFled).toBe(true);
});
it("wimp out will not skip battles when triggered in a double battle", async () => {
it("should not skip battles when triggered in a double battle", async () => {
const wave = 2;
game.override
.enemyMoveset(Moves.SPLASH)
@ -527,7 +487,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.currentBattle.waveIndex).toBe(wave + 1);
});
it("wimp out should not skip battles when triggering the same turn as another enemy faints", async () => {
it("should not skip battles when triggering the same turn as another enemy faints", async () => {
const wave = 2;
game.override
.enemySpecies(Species.WIMPOD)

View File

@ -44,20 +44,18 @@ describe("Moves - Focus Punch", () => {
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
const enemyStartingHp = enemyPokemon.hp;
game.move.select(Moves.FOCUS_PUNCH);
await game.phaseInterceptor.to(MessagePhase);
expect(enemyPokemon.hp).toBe(enemyStartingHp);
expect(enemyPokemon.getInverseHp()).toBe(0);
expect(leadPokemon.getMoveHistory().length).toBe(0);
await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
expect(enemyPokemon.getInverseHp()).toBe(0);
expect(leadPokemon.getMoveHistory().length).toBe(1);
expect(leadPokemon.turnData.totalDamageDealt).toBe(enemyStartingHp - enemyPokemon.hp);
expect(enemyPokemon.getInverseHp()).toBeGreaterThan(0);
});
it("should fail if the user is hit", async () => {
@ -72,16 +70,16 @@ describe("Moves - Focus Punch", () => {
game.move.select(Moves.FOCUS_PUNCH);
await game.phaseInterceptor.to(MessagePhase);
await game.phaseInterceptor.to("MessagePhase");
expect(enemyPokemon.hp).toBe(enemyStartingHp);
expect(leadPokemon.getMoveHistory().length).toBe(0);
await game.phaseInterceptor.to(BerryPhase, false);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBe(enemyStartingHp);
expect(leadPokemon.getMoveHistory().length).toBe(1);
expect(leadPokemon.turnData.totalDamageDealt).toBe(0);
expect(enemyPokemon.getInverseHp()).toBe(0);
});
it("should be cancelled if the user falls asleep mid-turn", async () => {

View File

@ -178,7 +178,7 @@ describe("Moves - Powder", () => {
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
// enemy should have taken damage from player's Fiery Dance + 2 Powder procs
expect(enemyPokemon.hp).toBe(
enemyStartingHp - playerPokemon.turnData.totalDamageDealt - 2 * Math.floor(enemyPokemon.getMaxHp() / 4),
enemyStartingHp - playerPokemon.turnData.lastMoveDamageDealt - 2 * Math.floor(enemyPokemon.getMaxHp() / 4),
);
});