mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-12-24 18:49:16 +01:00
* Add failure conditions and move failures part 1 * Add second and third failure sequences * Refactor mostly complete, need to recheck tests * Adjust status checks to respect ignoreStatus useModes * Adjust restriction for stuff cheeks * Address bertie's review comments * Add counterRedirectAttr to other counter-like moves * Adjust some documentation for new methods * Make substitute use the move tag * Adjust counter attr to use array.find * Adjust move condition check that occurs in the third failure check sequence * Insert move failure check sequence part 4 into move phase * Revert type adjustment to getBattlerIndex * Make charging moves deduct pp on use instead of on release * Fix first move condition not using 1 based starting wave * Tweak charge move handling and protean timing * Adjust fly tests to expect pp reduction properly * Add missing attribute to counter * Adjust revival blessing hardcore test to respect new return value of isUsable * Adjust copycat test to account for how it actually works * Play sleep animation and message * Remove BYPASS_SLEEP battler tag in favor of boolean holder * Finish unfinished docs * Ensure move restrictions are only checked for players * Adjust pollen puff condition, fix docs on `isOpponent` * Fix failAgainstFinalBossCondition * Fix dig test * Adjust dive's test * Fix missing break in applyConditions * Fix getBattlerIndex for enemyPokemon * Adjust type hint test to not rely on teleport * Minor adjustments from code review Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Add tests for teleport * Minor adjustments from code review Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * PR review changes Fix type hints test name Update Dig/Dive test name Separate TSDoc imports in `pokemon-utils.ts` Add missing `@returns` in `move-phase.ts` Fix comment typos Separate TSDoc imports in `move-phase.ts` Add return hints to `trySelectMove` Minor formatting Remove duplicate `.affectedByGravity()` on Telekinesis Fix docs for `checkRestrictions` Manually format method definition Fix comment spacing Fix variable naming * Address kev's review comments Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Minor adjustments from code review Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Remove optional chaining * fix: type for InferKeys * chore: apply biome * chore: fix merge conflicts from Biome update * Remove latent isNullOrUndefined * Drop readonly on timingModifier * docs: Add class comment * Address comments from code review * Drop readonly from timingModifier * Cleanup proc chance computation * Move `cureStatus` into the Pokemon class * Final touchups --------- Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
265 lines
9.9 KiB
TypeScript
265 lines
9.9 KiB
TypeScript
// biome-ignore lint/correctness/noUnusedImports: Used in a TSDoc comment
|
|
import type { GameMode } from "#app/game-mode";
|
|
import { globalScene } from "#app/global-scene";
|
|
import { getPokemonNameWithAffix } from "#app/messages";
|
|
import { TrappedTag } from "#data/battler-tags";
|
|
import { allMoves } from "#data/data-lists";
|
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
import { Command } from "#enums/command";
|
|
import { MoveCategory, type MoveDamageCategory } from "#enums/move-category";
|
|
import type { MoveId } from "#enums/move-id";
|
|
import { isVirtual } from "#enums/move-use-mode";
|
|
import { PokemonType } from "#enums/pokemon-type";
|
|
import type { Pokemon } from "#field/pokemon";
|
|
import type { Move, MoveConditionFunc, UserMoveConditionFunc } from "#moves/move";
|
|
import { getCounterAttackTarget } from "#moves/move-utils";
|
|
import i18next from "i18next";
|
|
|
|
/**
|
|
* A condition that determines whether a move can be used successfully.
|
|
*
|
|
* @remarks
|
|
* This is only checked when the move is attempted to be invoked. To prevent a move from being selected,
|
|
* use a {@linkcode MoveRestriction} instead.
|
|
*/
|
|
export class MoveCondition {
|
|
public declare readonly func: MoveConditionFunc;
|
|
|
|
/**
|
|
* @param func - A condition function that determines if the move can be used successfully
|
|
*/
|
|
constructor(func: MoveConditionFunc) {
|
|
this.func = func;
|
|
}
|
|
|
|
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
|
return this.func(user, target, move);
|
|
}
|
|
|
|
getUserBenefitScore(_user: Pokemon, _target: Pokemon, _move: Move): number {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Condition to allow a move's use only on the first turn this Pokemon is sent into battle
|
|
* (or the start of a new wave, whichever comes first).
|
|
*/
|
|
export class FirstMoveCondition extends MoveCondition {
|
|
constructor() {
|
|
super(user => user.tempSummonData.waveTurnCount === 1);
|
|
}
|
|
|
|
// TODO: Update AI move selection logic to not require this method at all
|
|
// Currently, it is used to avoid having the AI select the move if its condition will fail
|
|
getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
|
|
return this.apply(user, _target, _move) ? 10 : -20;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Condition that fails the move if the user has less than 1/x of their max HP.
|
|
* @remarks
|
|
* Used by Clangorous Soul and Fillet Away
|
|
*
|
|
* NOT used by Belly Drum, whose failure check occurs in phase 4 along with its stat increase condition
|
|
*/
|
|
export class FailIfInsufficientHpCondition extends MoveCondition {
|
|
/**
|
|
* Condition that fails the move if the user has less than 1/x of their max HP.
|
|
* @param ratio - The required HP ratio (the `x` in `1/x`)
|
|
*/
|
|
constructor(cutRatio: number) {
|
|
super(user => user.getHpRatio() > 1 / cutRatio);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Teleport condition checks
|
|
*
|
|
* @remarks
|
|
* For trainer pokemon, just checks if there are any benched pokemon allowed in battle
|
|
*
|
|
* Wild pokemon cannot teleport if either:
|
|
* - The current battle is a double battle
|
|
* - They are under the effects of a *move-based* trapping effect like and are neither a ghost type nor have an active run away ability
|
|
*/
|
|
export const failTeleportCondition = new MoveCondition(user => {
|
|
if (user.hasTrainer()) {
|
|
const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
|
for (const pokemon of party) {
|
|
if (!pokemon.isOnField() && pokemon.isAllowedInBattle()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Wild pokemon
|
|
|
|
// Cannot teleport in double battles (even if last remaining)
|
|
if (globalScene.currentBattle.double) {
|
|
return false;
|
|
}
|
|
// If smoke ball / shed tail items are ever added, checks for them should be placed here
|
|
// If a conditional "run away" ability is ever added, then we should use the apply method instead of the `hasAbility`
|
|
if (user.isOfType(PokemonType.GHOST, true, true) || user.hasAbilityWithAttr("RunSuccessAbAttr")) {
|
|
return true;
|
|
}
|
|
|
|
// Wild pokemon are prevented from fleeing if they are trapped *specifically*
|
|
if (globalScene.arena.hasTag(ArenaTagType.FAIRY_LOCK) || user.getTag(TrappedTag) !== undefined) {
|
|
// Fairy Lock prevents teleporting
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
/**
|
|
* Condition that forces moves to fail if the target's selected move is not an attacking move
|
|
*
|
|
* @remarks
|
|
* Used by Sucker Punch and Thunderclap
|
|
*/
|
|
export const failIfTargetNotAttackingCondition = new MoveCondition((_user, target) => {
|
|
const turnCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
|
|
if (!turnCommand || !turnCommand.move) {
|
|
return false;
|
|
}
|
|
return (
|
|
turnCommand.command === Command.FIGHT
|
|
&& !target.turnData.acted
|
|
&& allMoves[turnCommand.move.move].category !== MoveCategory.STATUS
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Condition that forces moves to fail against the final boss in classic and the major boss in endless
|
|
* @remarks
|
|
* ⚠️ Only works reliably for single-target moves as only one target is provided; should not be used for multi-target moves
|
|
* @see {@linkcode GameMode.isBattleClassicFinalBoss}
|
|
* @see {@linkcode GameMode.isEndlessMinorBoss}
|
|
*/
|
|
export const failAgainstFinalBossCondition = new MoveCondition((_user, target) => {
|
|
const gameMode = globalScene.gameMode;
|
|
const currentWave = globalScene.currentBattle.waveIndex;
|
|
return !(
|
|
target.isEnemy()
|
|
&& (gameMode.isBattleClassicFinalBoss(currentWave) || gameMode.isEndlessMinorBoss(currentWave))
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Upper_Hand_(move) | Upper Hand}.
|
|
* Moves with this condition are only successful when the target has selected
|
|
* a high-priority attack (after factoring in priority-boosting effects) and
|
|
* hasn't moved yet this turn.
|
|
*/
|
|
export const upperHandCondition = new MoveCondition((_user, target) => {
|
|
const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
|
|
return (
|
|
targetCommand?.command === Command.FIGHT
|
|
&& !target.turnData.acted
|
|
&& !!targetCommand.move?.move
|
|
&& allMoves[targetCommand.move.move].category !== MoveCategory.STATUS
|
|
&& allMoves[targetCommand.move.move].getPriority(target) > 0
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Last_Resort_(move) | Last Resort}
|
|
*
|
|
* @remarks
|
|
* Last resort fails if
|
|
* - It is not in the user's moveset
|
|
* - The user does not know at least one other move
|
|
* - The user has not directly used each other move in its moveset since it was sent into battle
|
|
* - A move is considered *used* for this purpose if it passed the first failure check sequence in the move phase
|
|
* (i.e. its usage message was displayed)
|
|
*/
|
|
export const lastResortCondition = new MoveCondition((user, _target, move) => {
|
|
const otherMovesInMoveset = new Set<MoveId>(user.getMoveset().map(m => m.moveId));
|
|
if (!otherMovesInMoveset.delete(move.id) || otherMovesInMoveset.size === 0) {
|
|
return false; // Last resort fails if used when not in user's moveset or no other moves exist
|
|
}
|
|
|
|
const movesInHistory = new Set<MoveId>(
|
|
user
|
|
.getMoveHistory()
|
|
.filter(m => !isVirtual(m.useMode)) // Last resort ignores virtual moves
|
|
.map(m => m.move),
|
|
);
|
|
|
|
// Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion
|
|
return [...otherMovesInMoveset].every(m => movesInHistory.has(m));
|
|
});
|
|
|
|
/**
|
|
* Condition used by counter-like moves if the user was hit by at least one qualifying attack this turn.
|
|
* Qualifying attacks are those that match the specified category (physical, special or either)
|
|
* that did not come from an ally.
|
|
*/
|
|
class CounterAttackConditon extends MoveCondition {
|
|
/**
|
|
* @param damageCategory - The category of move to counter (physical or special), or `undefined` to counter both
|
|
*/
|
|
constructor(damageCategory?: MoveDamageCategory) {
|
|
super(user => getCounterAttackTarget(user, damageCategory) !== null);
|
|
}
|
|
}
|
|
|
|
/** Condition check for counterattacks that proc againt physical moves */
|
|
export const counterAttackConditionPhysical = new CounterAttackConditon(MoveCategory.PHYSICAL);
|
|
/** Condition check for counterattacks that proc against special moves */
|
|
export const counterAttackConditionSpecial = new CounterAttackConditon(MoveCategory.SPECIAL);
|
|
/** Condition check for counterattacks that proc against moves regardless of damage type */
|
|
export const counterAttackConditionBoth = new CounterAttackConditon();
|
|
|
|
/**
|
|
* A restriction that prevents a move from being selected
|
|
*
|
|
* @remarks
|
|
* Only checked when the move is selected, but not when it is attempted to be invoked. To prevent a move from being used,
|
|
* use a {@linkcode MoveCondition} instead.
|
|
*/
|
|
export class MoveRestriction {
|
|
public readonly func: UserMoveConditionFunc;
|
|
public readonly i18nkey: string;
|
|
constructor(func: UserMoveConditionFunc, i18nkey = "battle:moveRestricted") {
|
|
this.func = func;
|
|
this.i18nkey = i18nkey;
|
|
}
|
|
|
|
/**
|
|
* @param user - The Pokemon attempting to select the move
|
|
* @param move - The move being selected
|
|
* @returns Whether the move is restricted for the user.
|
|
*/
|
|
apply(user: Pokemon, move: Move): boolean {
|
|
return this.func(user, move);
|
|
}
|
|
|
|
public getSelectionDeniedText(user: Pokemon, move: Move): string {
|
|
// While not all restriction texts use all the parameters, passing extra ones is harmless
|
|
return i18next.t(this.i18nkey, { pokemonNameWithAffix: getPokemonNameWithAffix(user), moveName: move.name });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prevents a Pokemon from using the move if it was the last move it used
|
|
*
|
|
* @remarks
|
|
* Used by {@link https://bulbapedia.bulbagarden.net/wiki/Blood_Moon_(move) | Blood Moon} and {@link https://bulbapedia.bulbagarden.net/wiki/Gigaton_Hammer_(move) | Gigaton Hammer}
|
|
*/
|
|
export const consecutiveUseRestriction = new MoveRestriction(
|
|
(user, move) => user.getLastXMoves(1)[0]?.move === move.id,
|
|
"battle:moveDisabledConsecutive",
|
|
);
|
|
|
|
/** Prevents a move from being selected if Gravity is in effect */
|
|
export const gravityUseRestriction = new MoveRestriction(
|
|
() => globalScene.arena.hasTag(ArenaTagType.GRAVITY),
|
|
"battle:moveDisabledGravity",
|
|
);
|