[Ability] Implement Wimp Out and Emergency Exit (#4701)

* implement Wimp Out/Emergency Exit

* fixed test

* fixed weather bug

* Added nightmare interaction to Wimp Out following bug fix

* refactored and added postdamageattr

* bug fixes

* added confusion test back (skipped)

* updated applyPostDamageAbAttrs to applyPostDamage

* fix external func name

* fixed syntax inconsistency

* updated PostDamageForceSwitchAttr -> PostDamageForceSwitchAbAttr

* Modify `wimp_out.test.ts`

* remove extra comment

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* remove extra comment

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update tsdocs

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* remove comment

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* remove comment

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* fix tsdocs

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* fix tsdocs

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* fix tsdocs

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* fix tsdocs

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* fix whitespace

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* make getFailedText public

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* make switchOutLogic public

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* make getSwitchOutCondition public

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* make getFailedText public

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* make applyPostDamage public

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* simplify if statement

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* add public override to applyPostDamage

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* fixed nested if issue, remove trapped tag removal

* add fix for multi hit move

* added multi-lens logic

* moved applyPostDamageAbAttrs to pokemon.damage, added check for multi lens in pokemon.apply

* added source to damageAndUpdate and applyPostDamageAbAttrs, added Parental Bond logic + test, put applyPostDamageAbAttrs back in damageAndUpdate

* simplify multi hit check

Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com>

* Minor formatting changes

* Update src/data/ability.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* moved and renamed shouldPreventSwitchOut, rewrote tests to account for U-turn changes, fix syntax error

* Move comment slightly

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com>
This commit is contained in:
muscode 2024-11-03 01:04:20 -05:00 committed by GitHub
parent b17b1d6e7e
commit f0ae36de6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 921 additions and 19 deletions

View File

@ -1,4 +1,4 @@
import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { Type } from "./type"; import { Type } from "./type";
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
@ -9,7 +9,7 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g
import { Gender } from "./gender"; import { Gender } from "./gender";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
import i18next from "i18next"; import i18next from "i18next";
@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales";
import { Command } from "../ui/command-ui-handler"; import { Command } from "../ui/command-ui-handler";
import { BerryModifierType } from "#app/modifier/modifier-type"; import { BerryModifierType } from "#app/modifier/modifier-type";
import { getPokeballName } from "./pokeball"; import { getPokeballName } from "./pokeball";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex, BattleType } from "#app/battle";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
@ -29,6 +29,12 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { SwitchType } from "#app/enums/switch-type";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
export class Ability implements Localizable { export class Ability implements Localizable {
public id: Abilities; public id: Abilities;
@ -3092,7 +3098,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
/** /**
* Condition function to applied to abilities related to Sheer Force. * 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: * Checks if last move used against target was affected by a Sheer Force user and:
* Disables: Color Change, Pickpocket, Wimp Out, Emergency Exit, Berserk, Anger Shell * Disables: Color Change, Pickpocket, Berserk, Anger Shell
* @returns {AbAttrCondition} If false disables the ability which the condition is applied to. * @returns {AbAttrCondition} If false disables the ability which the condition is applied to.
*/ */
function getSheerForceHitDisableAbCondition(): AbAttrCondition { function getSheerForceHitDisableAbCondition(): AbAttrCondition {
@ -4833,6 +4839,239 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
} }
} }
class ForceSwitchOutHelper {
constructor(private switchType: SwitchType) {}
/**
* Handles the logic for switching out a Pokémon based on battle conditions, HP, and the switch type.
*
* @param pokemon The {@linkcode Pokemon} attempting to switch out.
* @returns `true` if the switch is successful
*/
public switchOutLogic(pokemon: Pokemon): boolean {
const switchOutTarget = pokemon;
/**
* If the switch-out target is a player-controlled Pokémon, the function checks:
* - Whether there are available party members to switch in.
* - 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.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
pokemon.scene.prependToPhase(new SwitchPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
return true;
}
/**
* For non-wild battles, it checks if the opposing party has any available Pokémon to switch in.
* If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated.
*/
} else if (pokemon.scene.currentBattle.battleType !== BattleType.WILD) {
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
pokemon.scene.prependToPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(),
(pokemon.scene.currentBattle.trainer ? pokemon.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false, false), MoveEndPhase);
return true;
}
/**
* For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles).
*/
} else {
if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false);
pokemon.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
if (switchOutTarget.scene.currentBattle.double) {
const allyPokemon = switchOutTarget.getAlly();
switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon);
}
}
if (!switchOutTarget.getAlly()?.isActive(true)) {
pokemon.scene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) {
pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene));
pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene));
}
}
}
return false;
}
/**
* Determines if a Pokémon can switch out based on its status, the opponent's status, and battle conditions.
*
* @param pokemon The Pokémon attempting to switch out.
* @param opponent The opponent Pokémon.
* @returns `true` if the switch-out condition is met
*/
public getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean {
const switchOutTarget = pokemon;
const player = switchOutTarget instanceof PlayerPokemon;
if (player) {
const blockedByAbility = new Utils.BooleanHolder(false);
applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility);
return !blockedByAbility.value;
}
if (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) {
if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) {
return false;
}
}
if (!player && pokemon.scene.currentBattle.isBattleMysteryEncounter() && !pokemon.scene.currentBattle.mysteryEncounter?.fleeAllowed) {
return false;
}
const party = player ? pokemon.scene.getParty() : pokemon.scene.getEnemyParty();
return (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD)
|| party.filter(p => p.isAllowedInBattle()
&& (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > pokemon.scene.currentBattle.getBattlerCount();
}
/**
* Returns a message if the switch-out attempt fails due to ability effects.
*
* @param target The target Pokémon.
* @returns The failure message, or `null` if no failure.
*/
public getFailedText(target: Pokemon): string | null {
const blockedByAbility = new Utils.BooleanHolder(false);
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null;
}
}
/**
* Calculates the amount of recovery from the Shell Bell item.
*
* If the Pokémon is holding a Shell Bell, this function computes the amount of health
* recovered based on the damage dealt in the current turn. The recovery is multiplied by the
* 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.
*/
function calculateShellBellRecovery(pokemon: Pokemon): number {
const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier);
if (shellBellModifier) {
return Utils.toDmgValue(pokemon.turnData.damageDealt / 8) * shellBellModifier.stackCount;
}
return 0;
}
/**
* Triggers after the Pokemon takes any damage
* @extends AbAttr
*/
export class PostDamageAbAttr extends AbAttr {
public applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise<boolean> {
return false;
}
}
/**
* Ability attribute for forcing a Pokémon to switch out after its health drops below half.
* 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
*
* @extends PostDamageAbAttr
* @see {@linkcode applyPostDamage}
*/
export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
private helper: ForceSwitchOutHelper = new ForceSwitchOutHelper(SwitchType.SWITCH);
private hpRatio: number;
constructor(hpRatio: number = 0.5) {
super();
this.hpRatio = hpRatio;
}
/**
* Applies the switch-out logic after the Pokémon takes damage.
* Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and
* the Pokémon's health after damage to determine whether the switch-out should occur.
*
* @param pokemon The Pokémon that took damage.
* @param damage The amount of damage taken by the Pokémon.
* @param passive N/A
* @param simulated Whether the ability is being simulated.
* @param args N/A
* @param source The Pokemon that dealt damage
* @returns `true` if the switch-out logic was successfully applied
*/
public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise<boolean> {
const moveHistory = pokemon.getMoveHistory();
// 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)) {
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) {
return false;
// Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force.
} 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) {
// Activates if it falls below half and recovers back above half from a Shell Bell
const shellBellHeal = calculateShellBellRecovery(pokemon);
if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) {
for (const opponent of pokemon.getOpponents()) {
if (!this.helper.getSwitchOutCondition(pokemon, opponent)) {
return false;
}
}
return this.helper.switchOutLogic(pokemon);
} else {
return false;
}
} else {
return false;
}
}
public getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
return this.helper.getFailedText(target);
}
}
export function applyAbAttrs(attrType: Constructor<AbAttr>, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise<void> { export function applyAbAttrs(attrType: Constructor<AbAttr>, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated); return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated);
} }
@ -4866,6 +5105,11 @@ export function applyPostSetStatusAbAttrs(attrType: Constructor<PostSetStatusAbA
return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated);
} }
export function applyPostDamageAbAttrs(attrType: Constructor<PostDamageAbAttr>,
pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[], source?: Pokemon): Promise<void> {
return applyAbAttrsInternal<PostDamageAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), args);
}
/** /**
* Applies a field Stat multiplier attribute * Applies a field Stat multiplier attribute
* @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
@ -5607,11 +5851,11 @@ export function initAbilities() {
new Ability(Abilities.STAMINA, 7) new Ability(Abilities.STAMINA, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(Abilities.WIMP_OUT, 7) new Ability(Abilities.WIMP_OUT, 7)
.condition(getSheerForceHitDisableAbCondition()) .attr(PostDamageForceSwitchAbAttr)
.unimplemented(), .edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.EMERGENCY_EXIT, 7) new Ability(Abilities.EMERGENCY_EXIT, 7)
.condition(getSheerForceHitDisableAbCondition()) .attr(PostDamageForceSwitchAbAttr)
.unimplemented(), .edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.WATER_COMPACTION, 7) new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7) new Ability(Abilities.MERCILESS, 7)

View File

@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { WeatherType } from "./weather"; import { WeatherType } from "./weather";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle"; import { BattlerIndex, BattleType } from "../battle";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
@ -5761,6 +5761,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
} }
} }
export class ForceSwitchOutAttr extends MoveEffectAttr { export class ForceSwitchOutAttr extends MoveEffectAttr {
constructor( constructor(
private selfSwitch: boolean = false, private selfSwitch: boolean = false,
@ -5779,12 +5780,19 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
return false; return false;
} }
/**
* Move the switch out logic inside the conditional block
* This ensures that the switch out only happens when the conditions are met
*/
const switchOutTarget = this.selfSwitch ? user : target; const switchOutTarget = this.selfSwitch ? user : target;
if (switchOutTarget instanceof PlayerPokemon) { if (switchOutTarget instanceof PlayerPokemon) {
/**
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
* If it did, the user of U-turn or Volt Switch will not be switched out.
*/
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN)
) {
if (this.hpDroppedBelowHalf(target)) {
return false;
}
}
// Switch out logic for the player's Pokemon // Switch out logic for the player's Pokemon
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false; return false;
@ -5810,6 +5818,17 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
false, false), MoveEndPhase); false, false), MoveEndPhase);
} }
} else { } else {
/**
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
* If it did, the user of U-turn or Volt Switch will not be switched out.
*/
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) {
if (this.hpDroppedBelowHalf(target)) {
return false;
}
}
// Switch out logic for everything else (eg: WILD battles) // Switch out logic for everything else (eg: WILD battles)
if (user.scene.currentBattle.waveIndex % 10 === 0) { if (user.scene.currentBattle.waveIndex % 10 === 0) {
return false; return false;
@ -5902,6 +5921,21 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
return ret; return ret;
} }
/**
* Helper function to check if the Pokémon's health is below half after taking damage.
* Used for an edge case interaction with Wimp Out/Emergency Exit.
* If the Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.
*/
hpDroppedBelowHalf(target: Pokemon): boolean {
const pokemonHealth = target.hp;
const maxPokemonHealth = target.getMaxHp();
const damageTaken = target.turnData.damageTaken;
const initialHealth = pokemonHealth + damageTaken;
// Check if the Pokémon's health has dropped below half after the damage
return initialHealth >= maxPokemonHealth / 2 && pokemonHealth < maxPokemonHealth / 2;
}
} }
export class ChillyReceptionAttr extends ForceSwitchOutAttr { export class ChillyReceptionAttr extends ForceSwitchOutAttr {

View File

@ -12,7 +12,7 @@ import * as Utils from "#app/utils";
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type";
import { getLevelTotalExp } from "#app/data/exp"; import { getLevelTotalExp } from "#app/data/exp";
import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "#app/modifier/modifier"; import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#app/data/pokeball";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#app/data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr } from "#app/data/ability"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability";
import PokemonData from "#app/system/pokemon-data"; import PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
@ -2818,7 +2818,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* We explicitly require to ignore the faint phase here, as we want to show the messages * We explicitly require to ignore the faint phase here, as we want to show the messages
* about the critical hit and the super effective/not very effective messages before the faint phase. * about the critical hit and the super effective/not very effective messages before the faint phase.
*/ */
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, source);
if (damage > 0) { if (damage > 0) {
if (source.isPlayer()) { if (source.isPlayer()) {
@ -2831,6 +2831,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
source.turnData.currDamageDealt = damage; source.turnData.currDamageDealt = damage;
this.turnData.damageTaken += damage; this.turnData.damageTaken += damage;
this.battleData.hitCount++; this.battleData.hitCount++;
// Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit
if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) {
const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier);
if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) {
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
}
}
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
this.turnData.attacksReceived.unshift(attackResult); this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) { if (source.isPlayer() && !this.isPlayer()) {
@ -2918,7 +2927,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.destroySubstitute(); this.destroySubstitute();
this.resetSummonData(); this.resetSummonData();
} }
return damage; return damage;
} }
@ -2932,12 +2940,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
* @returns integer of damage done * @returns integer of damage done
*/ */
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer {
const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical);
this.scene.unshiftPhase(damagePhase); this.scene.unshiftPhase(damagePhase);
damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
// Damage amount may have changed, but needed to be queued before calling damage function // Damage amount may have changed, but needed to be queued before calling damage function
damagePhase.updateAmount(damage); damagePhase.updateAmount(damage);
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
return damage; return damage;
} }

View File

@ -1,6 +1,6 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; import { applyAbAttrs, applyPostDamageAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, PostDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability";
import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims";
import { getStatusEffectActivationText } from "#app/data/status-effect"; import { getStatusEffectActivationText } from "#app/data/status-effect";
import { BattleSpec } from "#app/enums/battle-spec"; import { BattleSpec } from "#app/enums/battle-spec";
@ -41,6 +41,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
pokemon.updateInfo(); pokemon.updateInfo();
applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []);
} }
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end()); new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end());
} else { } else {

View File

@ -0,0 +1,614 @@
import { BattlerIndex } from "#app/battle";
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import GameManager from "#app/test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Wimp Out", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.ability(Abilities.WIMP_OUT)
.enemySpecies(Species.NINJASK)
.enemyPassiveAbility(Abilities.NO_GUARD)
.startingLevel(90)
.enemyLevel(70)
.moveset([ Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE ])
.enemyMoveset(Moves.FALSE_SWIPE)
.disableCrits();
});
function confirmSwitch(): void {
const [ pokemon1, pokemon2 ] = game.scene.getParty();
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD);
expect(pokemon2.species.speciesId).toBe(Species.WIMPOD);
expect(pokemon2.isFainted()).toBe(false);
expect(pokemon2.getHpRatio()).toBeLessThan(0.5);
}
function confirmNoSwitch(): void {
const [ pokemon1, pokemon2 ] = game.scene.getParty();
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);
}
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
]);
const wimpod = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(wimpod.hp).toEqual(Math.floor(wimpod.getMaxHp() * 0.33 + 1));
confirmSwitch();
});
it("It makes wild pokemon flee if triggered", async () => {
game.override.enemyAbility(Abilities.WIMP_OUT);
await game.classicMode.startBattle([
Species.GOLISOPOD,
Species.TYRUNT
]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
enemyPokemon.hp *= 0.52;
game.move.select(Moves.FALSE_SWIPE);
await game.phaseInterceptor.to("BerryPhase");
const isVisible = enemyPokemon.visible;
const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true);
});
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;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(wimpod.hp).toEqual(1);
confirmNoSwitch();
});
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
]);
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TRAPPED)).toBeUndefined();
expect(game.scene.getParty()[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined();
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 () => {
game.override
.startingLevel(95)
.enemyMoveset([ Moves.U_TURN ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
const enemyPokemon = game.scene.getEnemyPokemon()!;
const hasFled = enemyPokemon.switchOutStatus;
expect(hasFled).toBe(false);
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 () => {
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;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1);
});
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
]);
const wimpod = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("SwitchSummonPhase", false);
expect(wimpod.summonData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT);
await game.phaseInterceptor.to("TurnEndPhase");
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 ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.move.select(Moves.HEAD_SMASH);
game.doSelectPartyPokemon(1);
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();
});
it("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: 3 },
{ name: "HEALING_CHARM", count: 5 },
]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.75;
game.move.select(Moves.DOUBLE_EDGE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getParty()[1].getHpRatio()).toBeGreaterThan(0.5);
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 () => {
game.override
.enemyAbility(Abilities.SHEER_FORCE)
.enemyMoveset(Moves.SLUDGE_BOMB)
.startingLevel(95);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.move.select(Moves.SPLASH);
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.70;
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
.passiveAbility(Abilities.MAGIC_GUARD)
.enemyMoveset([ Moves.LEECH_SEED ])
.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);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getParty()[0].getHpRatio()).toEqual(0.51);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD);
});
it("Wimp Out activating should not cancel a double battle", async () => {
game.override
.battleType("double")
.enemyAbility(Abilities.WIMP_OUT)
.enemyMoveset([ Moves.SPLASH ])
.enemyLevel(1);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
const enemyLeadPokemon = game.scene.getEnemyParty()[0];
const enemySecPokemon = game.scene.getEnemyParty()[1];
game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
const isVisibleLead = enemyLeadPokemon.visible;
const hasFledLead = enemyLeadPokemon.switchOutStatus;
const isVisibleSec = enemySecPokemon.visible;
const hasFledSec = enemySecPokemon.switchOutStatus;
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
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 () => {
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.getParty()[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
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
expect(enemyPokemon.turnData.hitCount).toBe(5);
confirmSwitch();
});
it("triggers after last hit of multi hit move (multi lens)", async () => {
game.override
.enemyMoveset(Moves.TACKLE)
.enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
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
]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
expect(enemyPokemon.turnData.hitCount).toBe(2);
confirmSwitch();
});
// 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 () => {
game.override
.moveset([ Moves.SWORDS_DANCE ])
.enemyMoveset([ Moves.SWAGGER ]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.hp *= 0.51;
playerPokemon.setStatStage(Stat.ATK, 6);
playerPokemon.addTag(BattlerTagType.CONFUSED);
// TODO: add helper function to force confusion self-hits
while (playerPokemon.getHpRatio() > 0.49) {
game.move.select(Moves.SWORDS_DANCE);
await game.phaseInterceptor.to("TurnEndPhase");
}
confirmNoSwitch();
});
});