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
*/
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<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
* 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 */)

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.
* 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.

View File

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

View File

@ -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();