diff --git a/src/data/moves/move-condition.ts b/src/data/moves/move-condition.ts index 5c06ab9bebe..13cded8834e 100644 --- a/src/data/moves/move-condition.ts +++ b/src/data/moves/move-condition.ts @@ -26,10 +26,8 @@ export class MoveCondition { /** * @param func - A condition function that determines if the move can be used successfully */ - constructor(func?: MoveConditionFunc) { - if (func) { - this.func = func; - } + constructor(func: MoveConditionFunc) { + this.func = func; } apply(user: Pokemon, target: Pokemon, move: Move): boolean { @@ -46,9 +44,9 @@ export class MoveCondition { */ export class FirstMoveCondition extends MoveCondition { - public override readonly func: MoveConditionFunc = user => { - return user.tempSummonData.waveTurnCount === 1; - }; + constructor() { + super(user => user.tempSummonData.waveTurnCount === 0 && user.tempSummonData.turnCount === 0); + } // 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 @@ -173,6 +171,7 @@ export const UpperHandCondition = new MoveCondition((_user, target) => { * - 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(user.getMoveset().map(m => m.moveId)); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index b9dc493064d..6aa7378e838 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -450,8 +450,8 @@ export abstract class Move implements Localizable { * @param restriction - The function or `MoveRestriction` that evaluates to `true` if the move is restricted from * being selected * @param i18nkey - The i18n key for the restriction text - * @param alsoCondition - If `true`, also adds a {@linkcode MoveCondition} that checks the same condition when the - * move is used; default `false` + * @param alsoCondition - If `true`, also adds an equivalent {@linkcode MoveCondition} that checks the same condition when the + * move is used (while taking care to invert the return value); default `false` * @param conditionSeq - The sequence number where the failure check occurs; default `4` * @returns `this` for method chaining */ @@ -11033,9 +11033,11 @@ export function initMoves() { .attr(EatBerryAttr, true) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .restriction( - user => globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length > 0, + user => globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length === 0, "battle:moveDisabledNoBerry", - true), + true, + 3 + ), new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, true /* NOT ADDED if already trapped */) diff --git a/src/enums/battler-tag-lapse-type.ts b/src/enums/battler-tag-lapse-type.ts index cc93d6c6782..f4fbcdda9a6 100644 --- a/src/enums/battler-tag-lapse-type.ts +++ b/src/enums/battler-tag-lapse-type.ts @@ -1,3 +1,5 @@ +import type { MoveUseMode } from "#enums/move-use-mode"; + /** * Enum representing the possible ways a given BattlerTag can activate and/or tick down. * Each tag can have multiple different behaviors attached to different lapse types. @@ -12,12 +14,13 @@ export enum BattlerTagLapseType { */ MOVE, /** - * Tag activates during (or just after) the first failure check sequence in the move phase. + * Tag activates during (or just after) the first failure check sequence in the move phase * * @remarks * * Note tags with this lapse type will lapse immediately after the first failure check sequence, - * regardless of whether the move was successful or not. + * regardless of whether the move was successful or not, but is skipped if the move is a + * {@linkcode MoveUseMode.FOLLOW_UP | follow-up} move. * * To only lapse the tag between the first and second failure check sequences, use * {@linkcode BattlerTagLapseType.MOVE} instead. diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index c5e9d6dfad1..787d2819f28 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -138,28 +138,44 @@ export class MovePhase extends BattlePhase { */ protected firstFailureCheck(): boolean { // A big if statement will handle the checks (that each have side effects!) in the correct order - if ( + return ( this.checkSleep() || this.checkFreeze() || this.checkPP() || this.checkValidity() || - this.checkTagCancel(BattlerTagType.TRUANT, true) || + this.checkTagCancel(BattlerTagType.TRUANT) || this.checkPreUseInterrupt() || this.checkTagCancel(BattlerTagType.FLINCHED) || - this.checkTagCancel(BattlerTagType.DISABLED, true) || + this.checkTagCancel(BattlerTagType.DISABLED) || this.checkTagCancel(BattlerTagType.HEAL_BLOCK) || this.checkTagCancel(BattlerTagType.THROAT_CHOPPED) || this.checkGravity() || - this.checkTagCancel(BattlerTagType.TAUNT, true) || + this.checkTagCancel(BattlerTagType.TAUNT) || this.checkTagCancel(BattlerTagType.IMPRISON) || this.checkTagCancel(BattlerTagType.CONFUSED) || this.checkPara() || this.checkTagCancel(BattlerTagType.INFATUATED) - ) { - this.handlePreMoveFailures(); - return true; - } - return false; + ); + } + + /** + * Follow up moves need to check a subset of the first failure checks + * + * @remarks + * + * Based on smogon battle mechanics research, checks happen in the following order: + * 1. Invalid move (skipped in pokerogue) + * 2. Move prevented by heal block + * 3. Move prevented by throat chop + * 4. Gravity + * 5. sky battle (unused in Pokerogue) + */ + protected followUpMoveFirstFailureCheck(): boolean { + return ( + this.checkTagCancel(BattlerTagType.HEAL_BLOCK) || + this.checkTagCancel(BattlerTagType.THROAT_CHOPPED) || + this.checkGravity() + ); } /** @@ -302,33 +318,29 @@ export class MovePhase extends BattlePhase { user.turnData.acted = true; const useMode = this.useMode; const ignoreStatus = isIgnoreStatus(useMode); - if (!ignoreStatus && this.firstFailureCheck()) { - // Lapse all other pre-move tags + if (!ignoreStatus) { + this.firstFailureCheck(); user.lapseTags(BattlerTagLapseType.PRE_MOVE); + // At this point, called moves should be decided. + // For now, this comment works as a placeholder until called moves are reworked + // For correct alignment with mainline, this SHOULD go here, and this phase SHOULD rewrite its own move + } else if (useMode === MoveUseMode.FOLLOW_UP) { + this.followUpMoveFirstFailureCheck(); + } + // If the first failure check did not pass, then the move is cancelled + // Note: This only checks `cancelled`, as `failed` should NEVER be set by anything in the first failure check + if (this.cancelled) { + this.handlePreMoveFailures(); this.end(); - - /* - On cartridge, certain things *react* to move failures, depending on failure reason - The following would happen at this time on cartridge: - - Steadfast giving user speed boost if failed due to flinch - - Protect, detect, ally switch, etc, resetting consecutive use count - - Rollout / ice ball "unlocking" - - protect / ally switch / other moves resetting their consecutive use count - - and many others - In Pokerogue, these are instead handled elsewhere, and generally work in a way that aligns with cartridge behavior - */ return; } - // Tags still need to be lapsed if no failure occured - user.lapseTags(BattlerTagLapseType.PRE_MOVE); + // If this is a follow-up move , at this point, we need to re-check a few conditions - // At this point, called moves should be decided. - // For now, this comment works as a placeholder until we rework how called moves are handled - // For correct alignment with mainline, this SHOULD go here, and this phase SHOULD rewrite its own move - - // If the first failure check passes, then thaw the user if its move will thaw it. - // The sleep message and animation are also played if the user is asleep but using a move anyway (snore, sleep talk, etc) - this.post1stFailSleepOrThaw(); + // If the first failure check passes (and this is not a sub-move) then thaw the user if its move will thaw it. + // The sleep message and animation should also play if the user is asleep but using a move anyway (snore, sleep talk, etc) + if (useMode !== MoveUseMode.FOLLOW_UP) { + this.post1stFailSleepOrThaw(); + } // Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats) if (isVirtual(useMode)) { @@ -618,10 +630,7 @@ export class MovePhase extends BattlePhase { * @param tag - The tag type whose lapse method will be called with {@linkcode BattlerTagLapseType.PRE_MOVE} * @param checkIgnoreStatus - Whether to check {@link isIgnoreStatus} for the current {@linkcode MoveUseMode} to skip this check */ - private checkTagCancel(tag: BattlerTagType, checkIgnoreStatus = false): boolean { - if (checkIgnoreStatus && isIgnoreStatus(this.useMode)) { - return false; - } + private checkTagCancel(tag: BattlerTagType): boolean { this.pokemon.lapseTag(tag, BattlerTagLapseType.PRE_MOVE); return this.cancelled; } diff --git a/test/abilities/cud-chew.test.ts b/test/abilities/cud-chew.test.ts index 2c1850d8e42..e62a8897e02 100644 --- a/test/abilities/cud-chew.test.ts +++ b/test/abilities/cud-chew.test.ts @@ -118,8 +118,10 @@ describe("Abilities - Cud Chew", () => { await game.classicMode.startBattle([SpeciesId.FARIGIRAF]); const farigiraf = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); farigiraf.hp = 1; // needed to allow berry procs vi.spyOn(farigiraf, "randBattleSeedInt").mockReturnValue(0); + vi.spyOn(enemy, "randBattleSeedInt").mockReturnValue(0); game.move.select(MoveId.STUFF_CHEEKS); await game.toNextTurn();