Adjust restriction for stuff cheeks

This commit is contained in:
Sirz Benjie 2025-08-18 20:28:26 -05:00
parent ce752c9d07
commit 92c7ece725
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
5 changed files with 63 additions and 48 deletions

View File

@ -26,10 +26,8 @@ export class MoveCondition {
/** /**
* @param func - A condition function that determines if the move can be used successfully * @param func - A condition function that determines if the move can be used successfully
*/ */
constructor(func?: MoveConditionFunc) { constructor(func: MoveConditionFunc) {
if (func) { this.func = func;
this.func = func;
}
} }
apply(user: Pokemon, target: Pokemon, move: Move): boolean { apply(user: Pokemon, target: Pokemon, move: Move): boolean {
@ -46,9 +44,9 @@ export class MoveCondition {
*/ */
export class FirstMoveCondition extends MoveCondition { export class FirstMoveCondition extends MoveCondition {
public override readonly func: MoveConditionFunc = user => { constructor() {
return user.tempSummonData.waveTurnCount === 1; super(user => user.tempSummonData.waveTurnCount === 0 && user.tempSummonData.turnCount === 0);
}; }
// TODO: Update AI move selection logic to not require this method at all // 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 // 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 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 * - 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 * - 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) => { export const lastResortCondition = new MoveCondition((user, _target, move) => {
const otherMovesInMoveset = new Set<MoveId>(user.getMoveset().map(m => m.moveId)); const otherMovesInMoveset = new Set<MoveId>(user.getMoveset().map(m => m.moveId));

View File

@ -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 * @param restriction - The function or `MoveRestriction` that evaluates to `true` if the move is restricted from
* being selected * being selected
* @param i18nkey - The i18n key for the restriction text * @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 * @param alsoCondition - If `true`, also adds an equivalent {@linkcode MoveCondition} that checks the same condition when the
* move is used; default `false` * 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` * @param conditionSeq - The sequence number where the failure check occurs; default `4`
* @returns `this` for method chaining * @returns `this` for method chaining
*/ */
@ -11033,9 +11033,11 @@ export function initMoves() {
.attr(EatBerryAttr, true) .attr(EatBerryAttr, true)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
.restriction( .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", "battle:moveDisabledNoBerry",
true), true,
3
),
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8) 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(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 */) .attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, true /* NOT ADDED if already trapped */)

View File

@ -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. * 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. * Each tag can have multiple different behaviors attached to different lapse types.
@ -12,12 +14,13 @@ export enum BattlerTagLapseType {
*/ */
MOVE, 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 * @remarks
* *
* Note tags with this lapse type will lapse immediately after the first failure check sequence, * 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 * To only lapse the tag between the first and second failure check sequences, use
* {@linkcode BattlerTagLapseType.MOVE} instead. * {@linkcode BattlerTagLapseType.MOVE} instead.

View File

@ -138,28 +138,44 @@ export class MovePhase extends BattlePhase {
*/ */
protected firstFailureCheck(): boolean { protected firstFailureCheck(): boolean {
// A big if statement will handle the checks (that each have side effects!) in the correct order // A big if statement will handle the checks (that each have side effects!) in the correct order
if ( return (
this.checkSleep() || this.checkSleep() ||
this.checkFreeze() || this.checkFreeze() ||
this.checkPP() || this.checkPP() ||
this.checkValidity() || this.checkValidity() ||
this.checkTagCancel(BattlerTagType.TRUANT, true) || this.checkTagCancel(BattlerTagType.TRUANT) ||
this.checkPreUseInterrupt() || this.checkPreUseInterrupt() ||
this.checkTagCancel(BattlerTagType.FLINCHED) || this.checkTagCancel(BattlerTagType.FLINCHED) ||
this.checkTagCancel(BattlerTagType.DISABLED, true) || this.checkTagCancel(BattlerTagType.DISABLED) ||
this.checkTagCancel(BattlerTagType.HEAL_BLOCK) || this.checkTagCancel(BattlerTagType.HEAL_BLOCK) ||
this.checkTagCancel(BattlerTagType.THROAT_CHOPPED) || this.checkTagCancel(BattlerTagType.THROAT_CHOPPED) ||
this.checkGravity() || this.checkGravity() ||
this.checkTagCancel(BattlerTagType.TAUNT, true) || this.checkTagCancel(BattlerTagType.TAUNT) ||
this.checkTagCancel(BattlerTagType.IMPRISON) || this.checkTagCancel(BattlerTagType.IMPRISON) ||
this.checkTagCancel(BattlerTagType.CONFUSED) || this.checkTagCancel(BattlerTagType.CONFUSED) ||
this.checkPara() || this.checkPara() ||
this.checkTagCancel(BattlerTagType.INFATUATED) 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; user.turnData.acted = true;
const useMode = this.useMode; const useMode = this.useMode;
const ignoreStatus = isIgnoreStatus(useMode); const ignoreStatus = isIgnoreStatus(useMode);
if (!ignoreStatus && this.firstFailureCheck()) { if (!ignoreStatus) {
// Lapse all other pre-move tags this.firstFailureCheck();
user.lapseTags(BattlerTagLapseType.PRE_MOVE); 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(); 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; return;
} }
// Tags still need to be lapsed if no failure occured // If this is a follow-up move , at this point, we need to re-check a few conditions
user.lapseTags(BattlerTagLapseType.PRE_MOVE);
// At this point, called moves should be decided. // If the first failure check passes (and this is not a sub-move) then thaw the user if its move will thaw it.
// For now, this comment works as a placeholder until we rework how called moves are handled // The sleep message and animation should also play if the user is asleep but using a move anyway (snore, sleep talk, etc)
// For correct alignment with mainline, this SHOULD go here, and this phase SHOULD rewrite its own move if (useMode !== MoveUseMode.FOLLOW_UP) {
this.post1stFailSleepOrThaw();
// 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();
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats) // Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
if (isVirtual(useMode)) { 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 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 * @param checkIgnoreStatus - Whether to check {@link isIgnoreStatus} for the current {@linkcode MoveUseMode} to skip this check
*/ */
private checkTagCancel(tag: BattlerTagType, checkIgnoreStatus = false): boolean { private checkTagCancel(tag: BattlerTagType): boolean {
if (checkIgnoreStatus && isIgnoreStatus(this.useMode)) {
return false;
}
this.pokemon.lapseTag(tag, BattlerTagLapseType.PRE_MOVE); this.pokemon.lapseTag(tag, BattlerTagLapseType.PRE_MOVE);
return this.cancelled; return this.cancelled;
} }

View File

@ -118,8 +118,10 @@ describe("Abilities - Cud Chew", () => {
await game.classicMode.startBattle([SpeciesId.FARIGIRAF]); await game.classicMode.startBattle([SpeciesId.FARIGIRAF]);
const farigiraf = game.field.getPlayerPokemon(); const farigiraf = game.field.getPlayerPokemon();
const enemy = game.field.getEnemyPokemon();
farigiraf.hp = 1; // needed to allow berry procs farigiraf.hp = 1; // needed to allow berry procs
vi.spyOn(farigiraf, "randBattleSeedInt").mockReturnValue(0); vi.spyOn(farigiraf, "randBattleSeedInt").mockReturnValue(0);
vi.spyOn(enemy, "randBattleSeedInt").mockReturnValue(0);
game.move.select(MoveId.STUFF_CHEEKS); game.move.select(MoveId.STUFF_CHEEKS);
await game.toNextTurn(); await game.toNextTurn();