Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Ability] Implement Wimp Out and Emergency Exit #4701

Merged
merged 51 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ba24d63
implement Wimp Out/Emergency Exit
muscode13 Oct 21, 2024
18dd5c1
Merge remote-tracking branch 'origin/beta' into ability/wimpout
muscode13 Oct 21, 2024
a677a71
fixed test
muscode13 Oct 22, 2024
f3fef6c
fixed weather bug
muscode13 Oct 22, 2024
82a6385
merge
muscode13 Oct 22, 2024
cce3818
Added nightmare interaction to Wimp Out following bug fix
muscode13 Oct 22, 2024
47a3428
refactored and added postdamageattr
muscode13 Oct 22, 2024
bf6e496
Merge remote-tracking branch 'origin/beta' into ability/wimpout
muscode13 Oct 22, 2024
e01b909
bug fixes
muscode13 Oct 23, 2024
d7b1155
Merge remote-tracking branch 'origin/beta' into ability/wimpout
muscode13 Oct 23, 2024
14ae2af
added confusion test back (skipped)
muscode13 Oct 23, 2024
b3ef771
Merge remote-tracking branch 'origin/beta' into ability/wimpout
muscode13 Oct 23, 2024
edf922d
updated applyPostDamageAbAttrs to applyPostDamage
muscode13 Oct 23, 2024
102c491
fix external func name
muscode13 Oct 24, 2024
34bf97a
fixed syntax inconsistency
muscode13 Oct 26, 2024
be5f154
updated PostDamageForceSwitchAttr -> PostDamageForceSwitchAbAttr
muscode13 Oct 27, 2024
1ef4329
Merge remote-tracking branch 'origin/beta' into ability/wimpout
muscode13 Oct 27, 2024
fa79c19
Modify `wimp_out.test.ts`
DayKev Nov 1, 2024
eb951f2
remove extra comment
muscode13 Nov 1, 2024
bd19966
remove extra comment
muscode13 Nov 1, 2024
5df72ee
Update tsdocs
muscode13 Nov 1, 2024
5761369
remove comment
muscode13 Nov 1, 2024
bb87a9a
remove comment
muscode13 Nov 1, 2024
d57c800
fix tsdocs
muscode13 Nov 1, 2024
c6e758e
fix tsdocs
muscode13 Nov 1, 2024
ce0305c
fix tsdocs
muscode13 Nov 1, 2024
34343a0
fix tsdocs
muscode13 Nov 1, 2024
e5a2a64
fix whitespace
muscode13 Nov 1, 2024
596a8b8
make getFailedText public
muscode13 Nov 1, 2024
6ef3003
make switchOutLogic public
muscode13 Nov 1, 2024
bad0494
make getSwitchOutCondition public
muscode13 Nov 1, 2024
0ff40e6
make getFailedText public
muscode13 Nov 1, 2024
809e710
make applyPostDamage public
muscode13 Nov 1, 2024
844fbe3
simplify if statement
muscode13 Nov 1, 2024
20ad144
add public override to applyPostDamage
muscode13 Nov 1, 2024
80315d2
fixed nested if issue, remove trapped tag removal
muscode13 Nov 1, 2024
747108f
Merge pull request #1 from DayKev/wimpout-patch-1
muscode13 Nov 1, 2024
699dbff
add fix for multi hit move
muscode13 Nov 1, 2024
bf8da08
Merge remote-tracking branch 'origin/beta' into ability/wimpout
muscode13 Nov 1, 2024
d8c54ff
added multi-lens logic
muscode13 Nov 1, 2024
38b7e6b
moved applyPostDamageAbAttrs to pokemon.damage, added check for multi…
muscode13 Nov 2, 2024
2d52b05
added source to damageAndUpdate and applyPostDamageAbAttrs, added Par…
muscode13 Nov 2, 2024
9305b4a
simplify multi hit check
muscode13 Nov 2, 2024
ba49157
Merge branch 'beta' into ability/wimpout
DayKev Nov 2, 2024
8db0d37
Minor formatting changes
DayKev Nov 2, 2024
075a14b
Merge branch 'beta' into ability/wimpout
DayKev Nov 2, 2024
2cbf7c5
Update src/data/ability.ts
muscode13 Nov 2, 2024
7af47c9
Merge branch 'beta' into ability/wimpout
DayKev Nov 2, 2024
3f6f52b
moved and renamed shouldPreventSwitchOut, rewrote tests to account fo…
muscode13 Nov 2, 2024
8e99d20
Move comment slightly
DayKev Nov 2, 2024
21fc752
Merge remote-tracking branch 'origin/beta' into ability/wimpout
muscode13 Nov 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 256 additions & 8 deletions src/data/ability.ts
Original file line number Diff line number Diff line change
@@ -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 { Constructor } from "#app/utils";
import * as Utils from "../utils";
Expand All @@ -9,15 +9,15 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g
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 { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
import i18next from "i18next";
import { Localizable } from "#app/interfaces/locales";
import { Command } from "../ui/command-ui-handler";
import { BerryModifierType } from "#app/modifier/modifier-type";
import { getPokeballName } from "./pokeball";
import { BattlerIndex } from "#app/battle";
import { BattlerIndex, BattleType } from "#app/battle";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
Expand All @@ -29,6 +29,12 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
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 {
public id: Abilities;
Expand Down Expand Up @@ -3065,7 +3071,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
/**
* Condition function to applied to abilities related to Sheer Force.
* Checks if last move used against target was affected by a Sheer Force user and:
* Disables: Color Change, Pickpocket, 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.
*/
function getSheerForceHitDisableAbCondition(): AbAttrCondition {
Expand Down Expand Up @@ -4731,6 +4737,243 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
}
}

//Helper class for Switch out logic
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
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} pokemon The Pokémon attempting to switch out.
* @returns {boolean} True if the switch is successful, false otherwise.
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
*/

switchOutLogic(pokemon: Pokemon): boolean {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
const switchOutTarget = pokemon;
// If the target is a Player Pokémon
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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;
}
// If the battle is not a wild Pokémon encounter
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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;
}
// If it's a wild Pokémon encounter
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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} pokemon The Pokémon attempting to switch out.
* @param {Pokemon} opponent The opponent Pokémon.
* @returns {boolean} True if the switch-out condition is met, false otherwise.
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
*/
getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
const switchOutTarget = pokemon;
const player = switchOutTarget instanceof PlayerPokemon;

if (switchOutTarget instanceof PlayerPokemon) {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
if (!player && pokemon.scene.currentBattle.isBattleMysteryEncounter() && !pokemon.scene.currentBattle.mysteryEncounter?.fleeAllowed) {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

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;
}
}

const party = player ? pokemon.scene.getParty() : pokemon.scene.getEnemyParty();
return (!player && !pokemon.scene.currentBattle.battleType)
DayKev marked this conversation as resolved.
Show resolved Hide resolved
|| 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 {Pokemon} target The target Pokémon.
* @returns {string | null} The failure message, or null if no failure.
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
*/
getFailedText(target: Pokemon): string | null {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
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} pokemon - The Pokémon whose Shell Bell recovery is being calculated.
* @returns {number} The amount of health recovered by Shell Bell.
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
*/
function calculateShellBellRecovery(pokemon: Pokemon): number {
DayKev marked this conversation as resolved.
Show resolved Hide resolved
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 {
applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
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.
*
* @extends PostDamageAbAttr
*/
export class PostDamageForceSwitchAttr extends PostDamageAbAttr {
private helper: ForceSwitchOutHelper;
DayKev marked this conversation as resolved.
Show resolved Hide resolved
private hpRatio: number;

constructor(hpRatio:number = 0.5) {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
super();
this.hpRatio = hpRatio;
this.helper = new ForceSwitchOutHelper(SwitchType.SWITCH);
DayKev marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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} pokemon The Pokémon that took damage.
* @param {number} damage The amount of damage taken by the Pokémon.
* @param {boolean} passive N/A
* @param {boolean} simulated Whether the ability is being simulated.
* @param {any[]} args N/A
* @returns {boolean | Promise<boolean>} True if the switch-out logic was successfully applied, false otherwise.
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
*/
applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
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];
// Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop.
DayKev marked this conversation as resolved.
Show resolved Hide resolved
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 ];
const getField = [ ...pokemon.getOpponents(), ...pokemon.getAlliedField() ];
for (const opponent of getField) {
const enemyMoveHistory = opponent.getMoveHistory();
if (enemyMoveHistory.length > 0) {
const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1];
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 && opponent.hasAbility(Abilities.SHEER_FORCE)) {
return false;
// Activate only after the last hit of multistrike moves
} else if (opponent.turnData.hitsLeft > 1) {
return false;
}
}
}


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() / 2) {
for (const opponent of pokemon.getOpponents()) {
if (!this.helper.getSwitchOutCondition(pokemon, opponent)) {
return false;
}
}
// Trapping moves do not prevent activation
if (pokemon.getTag(BattlerTagType.TRAPPED) !== undefined) {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
pokemon.removeTag(BattlerTagType.TRAPPED);
}
return this.helper.switchOutLogic(pokemon);
} else {
return false;
}
} else {
return false;
}
}
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
return this.helper.getFailedText(target);
}
}


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);
}
Expand Down Expand Up @@ -4764,6 +5007,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);
}

export function applyPostDamage(attrType: Constructor<PostDamageAbAttr>,
muscode13 marked this conversation as resolved.
Show resolved Hide resolved
pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[]): Promise<void> {
return applyAbAttrsInternal<PostDamageAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args), args);
}

/**
* Applies a field Stat multiplier attribute
* @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
Expand Down Expand Up @@ -5505,11 +5753,11 @@ export function initAbilities() {
new Ability(Abilities.STAMINA, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(Abilities.WIMP_OUT, 7)
.condition(getSheerForceHitDisableAbCondition())
.unimplemented(),
.attr(PostDamageForceSwitchAttr)
.edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.EMERGENCY_EXIT, 7)
.condition(getSheerForceHitDisableAbCondition())
.unimplemented(),
.attr(PostDamageForceSwitchAttr)
.edgeCase(), // Should not trigger when hurting itself in confusion
new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7)
Expand Down
Loading