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, NeutralDamageAgainstFlyingTypeMultiplierAttr,
FixedDamageAttr, FixedDamageAttr,
type MoveAttr, type MoveAttr,
ForceSwitchOutAttr,
} from "#app/data/moves/move"; } from "#app/data/moves/move";
import { ArenaTagSide } from "#app/data/arena-tag"; import { ArenaTagSide } from "#app/data/arena-tag";
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; 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. * 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, 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. * @returns An {@linkcode AbAttrCondition} to disable the ability under the proper conditions.
*/ */
function getSheerForceHitDisableAbCondition(): AbAttrCondition { function getSheerForceHitDisableAbCondition(): AbAttrCondition {
@ -5547,12 +5548,12 @@ function applySingleAbAttrs<TAttr extends AbAttr>(
* Shell Bell's modifier (if any). * Shell Bell's modifier (if any).
* *
* @param pokemon - The Pokémon whose Shell Bell recovery is being calculated. * @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 { function calculateShellBellRecovery(pokemon: Pokemon): number {
const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier);
if (shellBellModifier) { if (shellBellModifier) {
return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount; return toDmgValue(pokemon.turnData.lastMoveDamageDealt / 8) * shellBellModifier.stackCount;
} }
return 0; return 0;
} }
@ -5565,20 +5566,19 @@ export class PostDamageAbAttr extends AbAttr {
public canApplyPostDamage( public canApplyPostDamage(
pokemon: Pokemon, pokemon: Pokemon,
damage: number, damage: number,
passive: boolean,
simulated: boolean, simulated: boolean,
args: any[], source: Pokemon | undefined,
source?: Pokemon): boolean { args: any[]
): boolean {
return true; return true;
} }
public applyPostDamage( public applyPostDamage(
pokemon: Pokemon, pokemon: Pokemon,
damage: number, damage: number,
passive: boolean,
simulated: boolean, simulated: boolean,
args: any[], source: Pokemon | undefined,
source?: Pokemon, args: any[]
): void {} ): 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 * 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. * 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} * @see {@linkcode applyPostDamage}
*/ */
export class PostDamageForceSwitchAbAttr extends ForceSwitch(PostDamageAbAttr) { export class PostDamageForceSwitchAbAttr extends ForceSwitch(PostDamageAbAttr) {
private hpRatio: number; private hpRatio: number;
constructor(selfSwitch = true, switchType: NormalSwitchType = SwitchType.SWITCH, hpRatio = 0.5) { constructor(switchType: NormalSwitchType = SwitchType.SWITCH, hpRatio = 0.5) {
super(); super();
this.selfSwitch = selfSwitch; this.selfSwitch = false; // TODO: change if any abilities get damage
this.switchType = switchType; this.switchType = switchType;
this.hpRatio = hpRatio; 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( public override canApplyPostDamage(
pokemon: Pokemon, pokemon: Pokemon,
damage: number, damage: number,
passive: boolean, _simulated: boolean,
simulated: boolean, source: Pokemon | undefined,
args: any[], _args: any[],
source?: Pokemon): boolean { ): boolean {
const moveHistory = pokemon.getMoveHistory(); const userLastMove = pokemon.getLastXMoves()[0];
// Will not activate when the Pokémon's HP is lowered by cutting its own HP // 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 ]; const forbiddenAttackingMoves = new Set<Moves>([ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ]);
if (moveHistory.length > 0) { if (!isNullOrUndefined(userLastMove) && forbiddenAttackingMoves.has(userLastMove.move)) {
const lastMoveUsed = moveHistory[moveHistory.length - 1]; return false;
if (fordbiddenAttackingMoves.includes(lastMoveUsed.move)) {
return false;
}
} }
// Dragon Tail and Circle Throw switch out Pokémon before the Ability activates. // Skip last move checks if no enemy move
const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ]; const lastMove = source?.getLastXMoves()[0]
if (source) { if (
const enemyMoveHistory = source.getMoveHistory(); lastMove &&
if (enemyMoveHistory.length > 0) { // Will not activate for forced switch moves (triggers before wimp out activates)
const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1]; (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. // 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) { // TODO: Make this check the user's tags rather than the last move used by the target - we could be lifted by another pokemon
return false; || (lastMove.move === Moves.SKY_DROP && lastMove.result === MoveResult.OTHER))
// 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 return false;
} 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) { // Check for HP percents - don't switch if the move didn't knock us below our switch threshold
const shellBellHeal = calculateShellBellRecovery(pokemon); // (either because we were below it to begin with or are still above it after the hit).
if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) { const hpNeededToSwitch = pokemon.getMaxHp() * this.hpRatio;
for (const opponent of pokemon.getOpponents()) { if (pokemon.hp + damage < hpNeededToSwitch || pokemon.hp >= hpNeededToSwitch) {
if (!this.canSwitchOut(pokemon, opponent)) { return false;
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. * @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); this.doSwitch(pokemon);
} }
} }
@ -5837,16 +5827,15 @@ export function applyPostDamageAbAttrs(
attrType: Constructor<PostDamageAbAttr>, attrType: Constructor<PostDamageAbAttr>,
pokemon: Pokemon, pokemon: Pokemon,
damage: number, damage: number,
passive: boolean,
simulated = false, simulated = false,
args: any[], source: Pokemon | undefined = undefined,
source?: Pokemon, ...args: any[]
): void { ): void {
applyAbAttrsInternal<PostDamageAbAttr>( applyAbAttrsInternal<PostDamageAbAttr>(
attrType, attrType,
pokemon, pokemon,
(attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), (attr, passive) => attr.applyPostDamage(pokemon, damage, simulated, source, args),
(attr, passive) => attr.canApplyPostDamage(pokemon, damage, passive, simulated, args, source), (attr, passive) => attr.canApplyPostDamage(pokemon, damage, simulated, source, args),
args, args,
); );
} }
@ -6942,9 +6931,11 @@ export function initAbilities() {
.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)
.attr(PostDamageForceSwitchAbAttr) .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 .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) new Ability(Abilities.EMERGENCY_EXIT, 7)
.attr(PostDamageForceSwitchAbAttr) .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 .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) new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), .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; type MoveFilter = (move: Move) => boolean;
// TODO: fix this to check the last direct damage instance taken
export class CounterDamageAttr extends FixedDamageAttr { export class CounterDamageAttr extends FixedDamageAttr {
private moveFilter: MoveFilter; private moveFilter: MoveFilter;
private multiplier: number; private multiplier: number;
@ -1664,8 +1665,8 @@ export class RecoilAttr extends MoveEffectAttr {
return false; return false;
} }
const damageValue = (!this.useHp ? user.turnData.totalDamageDealt : user.getMaxHp()) * this.damageRatio; const damageValue = (!this.useHp ? user.turnData.lastMoveDamageDealt : user.getMaxHp()) * this.damageRatio;
const minValue = user.turnData.totalDamageDealt ? 1 : 0; const minValue = user.turnData.lastMoveDamageDealt ? 1 : 0;
const recoilDamage = toDmgValue(damageValue, minValue); const recoilDamage = toDmgValue(damageValue, minValue);
if (!recoilDamage) { if (!recoilDamage) {
return false; return false;

View File

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

View File

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

View File

@ -10,7 +10,6 @@ import {
IgnoreMoveEffectsAbAttr, IgnoreMoveEffectsAbAttr,
MaxMultiHitAbAttr, MaxMultiHitAbAttr,
PostAttackAbAttr, PostAttackAbAttr,
PostDamageAbAttr,
PostDefendAbAttr, PostDefendAbAttr,
ReflectStatusMoveAbAttr, ReflectStatusMoveAbAttr,
} from "#app/data/abilities/ability"; } from "#app/data/abilities/ability";
@ -48,7 +47,7 @@ import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/MoveCategory"; import { MoveCategory } from "#enums/MoveCategory";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { PokemonType } from "#enums/pokemon-type"; 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 type Pokemon from "#app/field/pokemon";
import { HitResult, MoveResult } from "#app/field/pokemon"; import { HitResult, MoveResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
@ -72,7 +71,7 @@ import { ShowAbilityPhase } from "./show-ability-phase";
import { MovePhase } from "./move-phase"; import { MovePhase } from "./move-phase";
import { MoveEndPhase } from "./move-end-phase"; import { MoveEndPhase } from "./move-end-phase";
import { HideAbilityPhase } from "#app/phases/hide-ability-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 { HitCheckResult } from "#enums/hit-check-result";
import type Move from "#app/data/moves/move"; import type Move from "#app/data/moves/move";
import { isFieldTargeted } from "#app/data/moves/move-utils"; import { isFieldTargeted } from "#app/data/moves/move-utils";
@ -101,6 +100,9 @@ export class MoveEffectPhase extends PokemonPhase {
/** Phases queued during moves */ /** Phases queued during moves */
private queuedPhases: Phase[] = []; 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 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) * @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.targets = targets;
this.hitChecks = Array(this.targets.length).fill([HitCheckResult.PENDING, 0]); 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) { if (this.lastHit) {
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); 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, isCritical,
}); });
// Apply and/or remove type boosting tags (Flash Fire, Charge, etc.)
const typeBoost = user.findTag( const typeBoost = user.findTag(
t => t instanceof TypeBoostTag && t.boostedType === user.getMoveType(this.move), t => t instanceof TypeBoostTag && t.boostedType === user.getMoveType(this.move),
) as TypeBoostTag; ) as TypeBoostTag;
@ -828,18 +827,17 @@ export class MoveEffectPhase extends PokemonPhase {
user.removeTag(typeBoost.tagType); user.removeTag(typeBoost.tagType);
} }
const isOneHitKo = result === HitResult.ONE_HIT_KO; if (dmg === 0) {
if (!dmg) {
return result; return result;
} }
const isOneHitKo = result === HitResult.ONE_HIT_KO;
target.lapseTags(BattlerTagLapseType.HIT); target.lapseTags(BattlerTagLapseType.HIT);
const substitute = target.getTag(SubstituteTag); const substituteTag = target.getTag(SubstituteTag);
const isBlockedBySubstitute = substitute && this.move.hitsSubstitute(user, target); const isBlockedBySubstitute = substituteTag && this.move.hitsSubstitute(user, target);
if (isBlockedBySubstitute) { if (isBlockedBySubstitute) {
substitute.hp -= dmg; substituteTag.hp -= dmg;
} else if (!target.isPlayer() && dmg >= target.hp) { } else if (!target.isPlayer() && dmg >= target.hp) {
globalScene.applyModifiers(EnemyEndureChanceModifier, false, target); globalScene.applyModifiers(EnemyEndureChanceModifier, false, target);
} }
@ -851,7 +849,6 @@ export class MoveEffectPhase extends PokemonPhase {
ignoreFaintPhase: true, ignoreFaintPhase: true,
ignoreSegments: isOneHitKo, ignoreSegments: isOneHitKo,
isCritical, isCritical,
source: user,
}); });
if (isCritical) { if (isCritical) {
@ -865,14 +862,13 @@ export class MoveEffectPhase extends PokemonPhase {
if (user.isPlayer()) { if (user.isPlayer()) {
globalScene.validateAchvs(DamageAchv, new NumberHolder(damage)); globalScene.validateAchvs(DamageAchv, new NumberHolder(damage));
if (damage > globalScene.gameData.gameStats.highestDamage) { globalScene.gameData.gameStats.highestDamage = Math.max(damage, globalScene.gameData.gameStats.highestDamage);
globalScene.gameData.gameStats.highestDamage = damage;
}
} }
user.turnData.totalDamageDealt += damage; user.turnData.lastMoveDamageDealt += damage;
user.turnData.singleHitDamageDealt = damage; user.turnData.singleHitDamageDealt = damage;
target.battleData.hitCount++; target.battleData.hitCount++;
// TODO: this might be incorrect for counter moves
target.turnData.damageTaken += damage; target.turnData.damageTaken += damage;
target.turnData.attacksReceived.unshift({ target.turnData.attacksReceived.unshift({

View File

@ -49,9 +49,10 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
} }
if (damage.value) { if (damage.value) {
// 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 ...
// TODO: why don't we call `damageAndUpdate` here?
globalScene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); globalScene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
pokemon.updateInfo(); 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()); new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(false, () => this.end());
} else { } else {

View File

@ -37,7 +37,7 @@ describe("Abilities - Wimp Out", () => {
.enemyPassiveAbility(Abilities.NO_GUARD) .enemyPassiveAbility(Abilities.NO_GUARD)
.startingLevel(90) .startingLevel(90)
.enemyLevel(70) .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) .enemyMoveset(Moves.FALSE_SWIPE)
.disableCrits(); .disableCrits();
}); });
@ -66,7 +66,40 @@ describe("Abilities - Wimp Out", () => {
expect(pokemon1.getHpRatio()).toBeLessThan(0.5); 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); game.override.passiveAbility(Abilities.REGENERATOR).startingLevel(5).enemyLevel(100);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -80,7 +113,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); 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); game.override.enemyAbility(Abilities.WIMP_OUT);
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
@ -95,7 +128,7 @@ describe("Abilities - Wimp Out", () => {
expect(!isVisible && hasFled).toBe(true); 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]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!; const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp = 5; wimpod.hp = 5;
@ -107,7 +140,7 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch(); 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); game.override.enemyMoveset([Moves.SPIRIT_SHACKLE]).startingLevel(53).enemyLevel(45);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -122,7 +155,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); 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]); game.override.startingLevel(95).enemyMoveset([Moves.U_TURN]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -136,7 +169,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); 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]); game.override.startingLevel(190).startingWave(8).enemyMoveset([Moves.U_TURN]);
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id; const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id;
@ -145,7 +178,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1); expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1);
}); });
it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => { it("should not activate from Dragon Tail and Circle Throw", async () => {
game.override.startingLevel(69).enemyMoveset([Moves.DRAGON_TAIL]); game.override.startingLevel(69).enemyMoveset([Moves.DRAGON_TAIL]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -162,81 +195,41 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD); expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD);
}); });
it("triggers when recoil damage is taken", async () => { it.each<{ type: string; enemyMove?: Moves; enemyAbility?: Abilities }>([
game.override.moveset([Moves.HEAD_SMASH]).enemyMoveset([Moves.SPLASH]); { type: "weather", enemyMove: Moves.HAIL },
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); { type: "status", enemyMove: Moves.TOXIC },
{ type: "Curse", enemyMove: Moves.CURSE },
game.move.select(Moves.HEAD_SMASH); { type: "Salt Cure", enemyMove: Moves.SALT_CURE },
game.doSelectPartyPokemon(1); { type: "partial trapping moves", enemyMove: Moves.WHIRLPOOL },
await game.phaseInterceptor.to("TurnEndPhase"); { type: "Leech Seed", enemyMove: Moves.LEECH_SEED },
{ type: "Nightmare", enemyMove: Moves.NIGHTMARE },
confirmSwitch(); { type: "Aftermath", enemyAbility: Abilities.AFTERMATH },
}); { type: "Bad Dreams", enemyAbility: Abilities.BAD_DREAMS },
])(
it("It does not activate when the Pokémon cuts its own HP", async () => { "should activate from damage caused by $name",
game.override.moveset([Moves.SUBSTITUTE]).enemyMoveset([Moves.SPLASH]); async ({ enemyMove = Moves.SPLASH, enemyAbility = Abilities.BALL_FETCH }) => {
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 game.override
.moveset([Moves.DOUBLE_EDGE]) .passiveAbility(Abilities.COMATOSE)
.enemyMoveset([Moves.SPLASH]) .enemySpecies(Species.GASTLY)
.startingHeldItems([{ name: "SHELL_BELL", count: 4 }]); .enemyMoveset(enemyMove)
.enemyAbility(enemyAbility)
.enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!; const wimpod = game.scene.getPlayerPokemon()!;
expect(wimpod).toBeDefined();
wimpod.hp *= 0.55;
wimpod.damageAndUpdate(toDmgValue(wimpod.getMaxHp() * 0.4)); game.move.select(Moves.THUNDER_PUNCH);
game.move.select(Moves.DOUBLE_EDGE);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerParty()[1]).toBe(wimpod); confirmSwitch();
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 () => { it("should not trigger from Sheer Force-boosted moves", 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); game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -248,81 +241,79 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch(); confirmNoSwitch();
}); });
it("Wimp Out will activate due to post turn status damage", async () => { it("should trigger from recoil damage", async () => {
game.override.statusEffect(StatusEffect.POISON).enemyMoveset([Moves.SPLASH]); game.override.moveset(Moves.HEAD_SMASH).enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51; game.move.select(Moves.HEAD_SMASH);
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
await game.toNextTurn(); await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch(); confirmSwitch();
}); });
it("Wimp Out will activate due to bad dreams", async () => { it("should trigger from Flame Burst ally damage in doubles", async () => {
game.override.statusEffect(StatusEffect.SLEEP).enemyAbility(Abilities.BAD_DREAMS); game.override.battleStyle("double").enemyMoveset([Moves.FLAME_BURST, Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); 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.move.select(Moves.SPLASH, BattlerIndex.PLAYER);
game.doSelectPartyPokemon(1); game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
await game.toNextTurn(); await game.forceEnemyMove(Moves.FLAME_BURST, BattlerIndex.PLAYER_2);
await game.forceEnemyMove(Moves.SPLASH);
game.doSelectPartyPokemon(2);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch(); confirmSwitch();
}); });
it("Wimp Out will activate due to leech seed", async () => { it("should not activate when the Pokémon cuts its own HP", async () => {
game.override.enemyMoveset([Moves.LEECH_SEED]); game.override.moveset(Moves.SUBSTITUTE).enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH); const wimpod = game.scene.getPlayerPokemon()!;
game.doSelectPartyPokemon(1); wimpod.hp *= 0.52;
await game.toNextTurn();
confirmSwitch(); game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
}); });
it("Wimp Out will activate due to curse damage", async () => { it("should not trigger when neutralized", async () => {
game.override.enemySpecies(Species.DUSKNOIR).enemyMoveset([Moves.CURSE]); game.override.enemyAbility(Abilities.NEUTRALIZING_GAS).startingLevel(5);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1); await game.phaseInterceptor.to("TurnEndPhase");
await game.toNextTurn();
confirmSwitch(); confirmNoSwitch();
}); });
it("Wimp Out will activate due to salt cure damage", async () => { it("should disregard Shell Bell recovery while still activating it before switching", async () => {
game.override.enemySpecies(Species.NACLI).enemyMoveset([Moves.SALT_CURE]).enemyLevel(1); game.override
.moveset([Moves.DOUBLE_EDGE])
.enemyMoveset([Moves.SPLASH])
.startingHeldItems([{ name: "SHELL_BELL", count: 4 }]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); 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); 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 () => { it("should not switch if Magic Guard prevents damage", async () => {
game.override.enemySpecies(Species.MAGIKARP).enemyMoveset([Moves.WHIRLPOOL]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.55;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override game.override
@ -341,7 +332,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD); 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); game.override.battleStyle("double").enemyAbility(Abilities.WIMP_OUT).enemyMoveset([Moves.SPLASH]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const enemyLeadPokemon = game.scene.getEnemyParty()[0]; const enemyLeadPokemon = game.scene.getEnemyParty()[0];
@ -361,24 +352,7 @@ describe("Abilities - Wimp Out", () => {
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp()); expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
}); });
it("Wimp Out will activate due to aftermath", async () => { it("should activate from entry hazard damage", 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.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override.enemySpecies(Species.CENTISKORCH).enemyAbility(Abilities.WIMP_OUT).startingWave(4); 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"); 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 () => { it("triggers status on the wimp out user before a new pokemon is switched in", async () => {
game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80); game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -413,7 +375,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); 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); game.override.enemyMoveset(Moves.BULLET_SEED).enemyAbility(Abilities.SKILL_LINK);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -429,7 +391,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch(); 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 }]); game.override.enemyMoveset(Moves.TACKLE).enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -444,6 +406,7 @@ describe("Abilities - Wimp Out", () => {
expect(enemyPokemon.turnData.hitCount).toBe(2); expect(enemyPokemon.turnData.hitCount).toBe(2);
confirmSwitch(); confirmSwitch();
}); });
it("triggers after last hit of Parental Bond", async () => { it("triggers after last hit of Parental Bond", async () => {
game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.PARENTAL_BOND); game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.PARENTAL_BOND);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -461,26 +424,23 @@ describe("Abilities - Wimp Out", () => {
}); });
// TODO: This interaction is not implemented yet // TODO: This interaction is not implemented yet
it.todo( it.todo("should not activate if the Pokémon's HP falls below half due to hurting itself in confusion", async () => {
"Wimp Out will not activate if the Pokémon's HP falls below half due to hurting itself in confusion", game.override.moveset([Moves.SWORDS_DANCE]).enemyMoveset([Moves.SWAGGER]);
async () => { await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.override.moveset([Moves.SWORDS_DANCE]).enemyMoveset([Moves.SWAGGER]); const playerPokemon = game.scene.getPlayerPokemon()!;
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); playerPokemon.hp *= 0.51;
const playerPokemon = game.scene.getPlayerPokemon()!; playerPokemon.setStatStage(Stat.ATK, 6);
playerPokemon.hp *= 0.51; playerPokemon.addTag(BattlerTagType.CONFUSED);
playerPokemon.setStatStage(Stat.ATK, 6);
playerPokemon.addTag(BattlerTagType.CONFUSED);
// TODO: add helper function to force confusion self-hits // TODO: add helper function to force confusion self-hits
while (playerPokemon.getHpRatio() > 0.49) { while (playerPokemon.getHpRatio() > 0.49) {
game.move.select(Moves.SWORDS_DANCE); game.move.select(Moves.SWORDS_DANCE);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
} }
confirmNoSwitch(); confirmNoSwitch();
}, });
);
it("should not activate on wave X0 bosses", async () => { it("should not activate on wave X0 bosses", async () => {
game.override.enemyAbility(Abilities.WIMP_OUT).startingLevel(5850).startingWave(10); game.override.enemyAbility(Abilities.WIMP_OUT).startingLevel(5850).startingWave(10);
@ -499,7 +459,7 @@ describe("Abilities - Wimp Out", () => {
expect(isVisible && !hasFled).toBe(true); 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; const wave = 2;
game.override game.override
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
@ -527,7 +487,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.currentBattle.waveIndex).toBe(wave + 1); 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; const wave = 2;
game.override game.override
.enemySpecies(Species.WIMPOD) .enemySpecies(Species.WIMPOD)

View File

@ -44,20 +44,18 @@ describe("Moves - Focus Punch", () => {
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
const enemyStartingHp = enemyPokemon.hp;
game.move.select(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(enemyPokemon.getInverseHp()).toBe(0);
expect(leadPokemon.getMoveHistory().length).toBe(0); expect(leadPokemon.getMoveHistory().length).toBe(0);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); expect(enemyPokemon.getInverseHp()).toBe(0);
expect(leadPokemon.getMoveHistory().length).toBe(1); 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 () => { it("should fail if the user is hit", async () => {
@ -72,16 +70,16 @@ describe("Moves - Focus Punch", () => {
game.move.select(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(enemyPokemon.hp).toBe(enemyStartingHp);
expect(leadPokemon.getMoveHistory().length).toBe(0); expect(leadPokemon.getMoveHistory().length).toBe(0);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBe(enemyStartingHp); expect(enemyPokemon.hp).toBe(enemyStartingHp);
expect(leadPokemon.getMoveHistory().length).toBe(1); 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 () => { 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()); expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
// enemy should have taken damage from player's Fiery Dance + 2 Powder procs // enemy should have taken damage from player's Fiery Dance + 2 Powder procs
expect(enemyPokemon.hp).toBe( expect(enemyPokemon.hp).toBe(
enemyStartingHp - playerPokemon.turnData.totalDamageDealt - 2 * Math.floor(enemyPokemon.getMaxHp() / 4), enemyStartingHp - playerPokemon.turnData.lastMoveDamageDealt - 2 * Math.floor(enemyPokemon.getMaxHp() / 4),
); );
}); });