From e86bdf9eb8e94be461ac6a5eaa302bcd301197ac Mon Sep 17 00:00:00 2001 From: geeil-han Date: Tue, 26 Nov 2024 07:47:10 +0100 Subject: [PATCH 01/10] Added full implementation of Stuff Cheeks --- src/data/move.ts | 75 +++++++++++- src/field/pokemon.ts | 18 +++ src/phases/command-phase.ts | 5 +- src/test/moves/stuff_cheeks.test.ts | 184 ++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 src/test/moves/stuff_cheeks.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index 2ac4d74b712f..e1bc6e1c7c26 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -142,6 +142,8 @@ export default class Move implements Localizable { public generation: number; public attrs: MoveAttr[] = []; private conditions: MoveCondition[] = []; + /** contains conditions if move is selectable and should fail or not */ + private selectableConditions: MoveSelectCondition[] = []; /** The move's {@linkcode MoveFlags} */ private flags: number = 0; private nameAppend: string = ""; @@ -374,6 +376,20 @@ export default class Move implements Localizable { return this; } + /** + * Adds a {@linkcode MoveSelectCondition} to the move if it is only selectable/successful under certain conditions + * @param condition {@linkcode MoveSelectCondition} appends to conditions array a MoveSelectCondition {@linkcode selectableConditions} + * @returns the called object {@linkcode Move} + */ + selectableCondition(condition: MoveSelectCondition): this { + console.log(`GHnote condition: ${typeof condition}`); + if (condition) { + this.selectableConditions.push(condition); + } + + return this; + } + /** * Internal dev flag for documenting edge cases. When using this, please document the known edge case. * @returns the called object {@linkcode Move} @@ -667,6 +683,21 @@ export default class Move implements Localizable { return true; } + /** + * Applies each {@linkcode MoveSelectCondition} function of this move to determine if the move can be used selected during {@linkcode CommandPhase} + * @param user {@linkcode Pokemon} to apply conditions to + * @returns boolean: false if any of the apply()'s return false, else true + */ + applySelectableConditions(user: Pokemon): boolean { + for (const condition of this.selectableConditions) { + if (!condition.apply(user, this)) { + return false; + } + } + + return true; + } + /** * Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move) * @param user {@linkcode Pokemon} using the move @@ -7496,6 +7527,44 @@ export function applyMoveChargeAttrs(attrType: Constructor, user: Poke return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); } +/** + * Base class defining all {@linkcode selectableConditions} + * Is used to add {@linkcode UserMoveConditionFunc} in order to check if condition + * is met and move can be selected/ if move fails + */ +export class MoveSelectCondition { + protected func: UserMoveConditionFunc; + + constructor(func: UserMoveConditionFunc) { + this.func = func; + } + + /** + * {@linkcode func} is being called in order to check if the {@linkcode user} is able to + * select the {@linkcode move} and if the move should fail + * + * @param user {@linkcode Pokemon} that want to use this {@linkcode move} + * @param move {@linkcode Move} being selected + * @returns true if the move can be selected/doesn't fail, otherwise false + */ + apply(user: Pokemon, move: Move): boolean { + return this.func(user, move); + } +} + +/** + * extends {@linkcode MoveSelectCondition} and contains the condition for {@link https://bulbapedia.bulbagarden.net/wiki/Stuff_Cheeks_(move) | Stuff Cheeks} + * success + */ +export class StuffCheeksCondition extends MoveSelectCondition { + /** + * contains function that checks if the {@linkcode user} is currently holding a berry or not + */ + constructor() { + super((user: Pokemon, move: Move) => user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length > 0); + } +} + export class MoveCondition { protected func: MoveConditionFunc; @@ -9989,11 +10058,7 @@ export function initMoves() { new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8) .attr(EatBerryAttr) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) - .condition((user) => { - const userBerries = user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()); - return userBerries.length > 0; - }) - .edgeCase(), // Stuff Cheeks should not be selectable when the user does not have a berry, see wiki + .selectableCondition(new StuffCheeksCondition()), new SelfStatusMove(Moves.NO_RETREAT, Type.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, false) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f9e7d7d1cad8..5b42981f7e42 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5355,6 +5355,10 @@ export class PokemonMove { return false; } + if (!this.isSelectable(pokemon)) { + return false; + } + if (this.getMove().name.endsWith(" (N)")) { return false; } @@ -5362,6 +5366,20 @@ export class PokemonMove { return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1); } + /** + * This function checks if the current move can be selected or not + * + * @param pokemon {@linkcode Pokemon} that selected this {@linkcode PokemonMove} + * @returns true if move can be selected, otherwise false + */ + isSelectable(pokemon: Pokemon): boolean { + const move = this.getMove(); + if (!move.applySelectableConditions(pokemon)) { + return false; + } + return true; + } + getMove(): Move { return allMoves[this.moveId]; } diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index eab76282908e..b4d67eae8b58 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -136,11 +136,12 @@ export class CommandPhase extends FieldPhase { const move = playerPokemon.getMoveset()[cursor]!; //TODO: is this bang correct? this.scene.ui.setMode(Mode.MESSAGE); - // Decides between a Disabled, Not Implemented, or No PP translation message + // Decides between a Disabled, not selectable, Not Implemented, or No PP translation message const errorMessage = playerPokemon.isMoveRestricted(move.moveId, playerPokemon) ? playerPokemon.getRestrictingTag(move.moveId, playerPokemon)!.selectionDeniedText(playerPokemon, move.moveId) - : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; + : !move.isSelectable(playerPokemon) ? "battle:moveCannotBeSelected" + : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => { diff --git a/src/test/moves/stuff_cheeks.test.ts b/src/test/moves/stuff_cheeks.test.ts new file mode 100644 index 000000000000..ce1d5f05d166 --- /dev/null +++ b/src/test/moves/stuff_cheeks.test.ts @@ -0,0 +1,184 @@ +import { Stat } from "#enums/stat"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import Pokemon from "#app/field/pokemon"; +import { allMoves, RandomMoveAttr } from "#app/data/move"; +import { BerryType } from "#enums/berry-type"; +import { BattlerIndex } from "#app/battle"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Stuff Cheeks", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + /** + * Count the number of held items a Pokemon has, accounting for stacks of multiple items. + */ + function getHeldItemCount(pokemon: Pokemon): number { + const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount()); + if (stackCounts.length) { + return stackCounts.reduce((a, b) => a + b); + } else { + return 0; + } + } + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .disableCrits() + .moveset(Moves.STUFF_CHEEKS) + .ability(Abilities.BALL_FETCH) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should succeed if berries are held", async () => { + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + ]); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.STUFF_CHEEKS); + + await game.toNextTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(2); + expect(getHeldItemCount(player)).toBe(0); + }); + + it("should fail if no berries are held", async () => { + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.STUFF_CHEEKS); + + await game.toNextTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(0); + expect(getHeldItemCount(player)).toBe(0); + }); + + it("should succeed when called in the presence of unnerved", async () => { + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 2 }, + { name: "BERRY", type: BerryType.LUM, count: 2 }, + ]) + .enemyAbility(Abilities.UNNERVE); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.STUFF_CHEEKS); + + await game.toNextTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(2); + expect(getHeldItemCount(player)).toBe(3); + }); + + it("should succeed when called in the presence of magic room", async () => { + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 2 }, + { name: "BERRY", type: BerryType.LUM, count: 2 }, + ]) + .enemyMoveset(Moves.MAGIC_ROOM); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.STUFF_CHEEKS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + + await game.toNextTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(2); + expect(getHeldItemCount(player)).toBe(3); + }); + + it("should failed when called by another move (metronome) while holding no berries", async () => { + game.override.moveset(Moves.METRONOME); + + const randomMoveAttr = allMoves[Moves.METRONOME].findAttr(attr => attr instanceof RandomMoveAttr) as RandomMoveAttr; + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.STUFF_CHEEKS); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.METRONOME); + + await game.toNextTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(0); + }); + + it("should succeed when called by another move (metronome) while holding berries", async () => { + game.override + .moveset(Moves.METRONOME) + .startingHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 2 }, + { name: "BERRY", type: BerryType.LUM, count: 2 }, + ]); + + const randomMoveAttr = allMoves[Moves.METRONOME].findAttr(attr => attr instanceof RandomMoveAttr) as RandomMoveAttr; + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.STUFF_CHEEKS); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.METRONOME); + + await game.toNextTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(2); + expect(getHeldItemCount(player)).toBe(3); + }); + + // Can be enabled when Knock off correctly knocks off the held berry + it.todo("should fail when used after berries getting knocked off", async () => { + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + ]) + .enemyMoveset(Moves.KNOCK_OFF); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.STUFF_CHEEKS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + + await game.toNextTurn(); + + expect(getHeldItemCount(player)).toBe(0); + expect(player.getStatStage(Stat.DEF)).toBe(0); + }); +}); From 31fea8eafc4d1cb580cabdb10babacc36de7d362 Mon Sep 17 00:00:00 2001 From: geeil-han Date: Tue, 26 Nov 2024 08:16:05 +0100 Subject: [PATCH 02/10] removed console.log for debugging --- src/data/move.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data/move.ts b/src/data/move.ts index e1bc6e1c7c26..106e1cb6811d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -382,7 +382,6 @@ export default class Move implements Localizable { * @returns the called object {@linkcode Move} */ selectableCondition(condition: MoveSelectCondition): this { - console.log(`GHnote condition: ${typeof condition}`); if (condition) { this.selectableConditions.push(condition); } From 7b5dc229a10ba10ee650a1ad95c4dab339089ab6 Mon Sep 17 00:00:00 2001 From: geeil-han Date: Sun, 1 Dec 2024 06:44:04 +0100 Subject: [PATCH 03/10] Refactoring implementation using MoveRestrictionTag --- src/data/battler-tags.ts | 28 +++++++++++++++++++++++ src/data/move.ts | 43 +++++++++++++++++++++++++++-------- src/enums/battler-tag-type.ts | 1 + src/field/pokemon.ts | 18 --------------- src/phases/command-phase.ts | 16 +++++++++++-- 5 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index ce25b56157c8..bfbb42de324d 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2782,6 +2782,32 @@ export class ImprisonTag extends MoveRestrictionBattlerTag { } } +export class StuffCheeksTag extends MoveRestrictionBattlerTag { + // Stuff Cheeks + private moveId: Moves = Moves.STUFF_CHEEKS; + /** + * This Tag only lasts the turn the {@linkcode MoveUnselectableTag} was added. + * @param move {@linkcode Moves} that is selected + */ + constructor(move: Moves) { + super(BattlerTagType.STUFF_CHEEKS, BattlerTagLapseType.TURN_END, 0, Moves.STUFF_CHEEKS); + } + + /** + * This function always returns true since {@linkcode MoveUnselectableTag} is only added if condition {@linkcode MoveSelectCondition} fails + * @param move {@linkcode Moves} that is selected + * @returns true if the move matches the ID of Stuff Cheeks + */ + override isMoveRestricted(move: Moves): boolean { + return move === this.moveId; + } + + override selectionDeniedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name }); + } +} + + /** * Battler Tag that applies the effects of Syrup Bomb to the target Pokemon. * For three turns, starting from the turn of hit, at the end of each turn, the target Pokemon's speed will decrease by 1. @@ -3123,6 +3149,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new GrudgeTag(); case BattlerTagType.PSYCHO_SHIFT: return new PsychoShiftTag(); + case BattlerTagType.STUFF_CHEEKS: + return new StuffCheeksTag(sourceMove); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index 106e1cb6811d..2b916a0414a3 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7539,15 +7539,13 @@ export class MoveSelectCondition { } /** - * {@linkcode func} is being called in order to check if the {@linkcode user} is able to - * select the {@linkcode move} and if the move should fail - * - * @param user {@linkcode Pokemon} that want to use this {@linkcode move} - * @param move {@linkcode Move} being selected - * @returns true if the move can be selected/doesn't fail, otherwise false + * Checks condition and adds appropriate MoveRestrictionTag accordingly + * @param user {@linkcode Pokemon} that uses the move + * @param move {@linkcode Move} that is used + * @returns true if Tag was added successfully */ apply(user: Pokemon, move: Move): boolean { - return this.func(user, move); + return true; } } @@ -7560,10 +7558,36 @@ export class StuffCheeksCondition extends MoveSelectCondition { * contains function that checks if the {@linkcode user} is currently holding a berry or not */ constructor() { - super((user: Pokemon, move: Move) => user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length > 0); + super((user, move) => this.selectableCondition(user)); + } + + /** + * Checks if the user is holding a berry + * @param user {@linkcode Pokemon} whose berries to check + * @returns true if the user is holding a berry, otherwise false + */ + private selectableCondition(user: Pokemon): boolean { + return user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length > 0; + } + + /** + * {@linkcode func} is being called in order to check if the {@linkcode user} is able to + * select the {@linkcode move} and adds {@linkcode StuffCheeksTag} if condition fails + * + * @param user {@linkcode Pokemon} that want to use this {@linkcode move} + * @param move {@linkcode Move} being selected + * @returns true if the move can be selected/doesn't fail, otherwise false + */ + apply(user: Pokemon, move: Move): boolean { + if (!this.selectableCondition(user)) { + user.addTag(BattlerTagType.STUFF_CHEEKS, 0, move.id); + } + return this.func(user, move); } } +const hasBerryCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length > 0; + export class MoveCondition { protected func: MoveConditionFunc; @@ -10057,7 +10081,8 @@ export function initMoves() { new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8) .attr(EatBerryAttr) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) - .selectableCondition(new StuffCheeksCondition()), + .selectableCondition(new StuffCheeksCondition()) + .condition(hasBerryCondition), new SelfStatusMove(Moves.NO_RETREAT, Type.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, false) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index bb969386630d..dd6e395ef05d 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -93,4 +93,5 @@ export enum BattlerTagType { GRUDGE = "GRUDGE", PSYCHO_SHIFT = "PSYCHO_SHIFT", ENDURE_TOKEN = "ENDURE_TOKEN", + STUFF_CHEEKS = "STUFF_CHEEKS", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 79859e058cc4..961cb9437310 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5368,10 +5368,6 @@ export class PokemonMove { return false; } - if (!this.isSelectable(pokemon)) { - return false; - } - if (this.getMove().name.endsWith(" (N)")) { return false; } @@ -5379,20 +5375,6 @@ export class PokemonMove { return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1); } - /** - * This function checks if the current move can be selected or not - * - * @param pokemon {@linkcode Pokemon} that selected this {@linkcode PokemonMove} - * @returns true if move can be selected, otherwise false - */ - isSelectable(pokemon: Pokemon): boolean { - const move = this.getMove(); - if (!move.applySelectableConditions(pokemon)) { - return false; - } - return true; - } - getMove(): Move { return allMoves[this.moveId]; } diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index b4d67eae8b58..9453c2976153 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -75,6 +75,19 @@ export class CommandPhase extends FieldPhase { const moveQueue = playerPokemon.getMoveQueue(); + /** + * Checks if the playerPokemon has a move that might be unselectable + */ + const moveset = playerPokemon.getMoveset(); + const conditionalMove = moveset.find(m => { + const move = m?.getMove(); + return move && move.selectableCondition && move.selectableCondition.length > 0; + }); + + if (conditionalMove) { + conditionalMove.getMove().applySelectableConditions(playerPokemon); + } + while (moveQueue.length && moveQueue[0] && moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move) || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move)]!.isUsable(playerPokemon, moveQueue[0].ignorePP))) { // TODO: is the bang correct? @@ -140,8 +153,7 @@ export class CommandPhase extends FieldPhase { const errorMessage = playerPokemon.isMoveRestricted(move.moveId, playerPokemon) ? playerPokemon.getRestrictingTag(move.moveId, playerPokemon)!.selectionDeniedText(playerPokemon, move.moveId) - : !move.isSelectable(playerPokemon) ? "battle:moveCannotBeSelected" - : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; + : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => { From ef46644d578b9c9609f50156c466ea4176c531e2 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:02:15 +0100 Subject: [PATCH 04/10] Updated documentation in move.ts --- src/data/move.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 941e101beeab..47a84690e2c6 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -142,7 +142,7 @@ export default class Move implements Localizable { public generation: number; public attrs: MoveAttr[] = []; private conditions: MoveCondition[] = []; - /** contains conditions if move is selectable and should fail or not */ + /** contains conditions if move is selectable or not */ private selectableConditions: MoveSelectCondition[] = []; /** The move's {@linkcode MoveFlags} */ private flags: number = 0; @@ -377,9 +377,9 @@ export default class Move implements Localizable { } /** - * Adds a {@linkcode MoveSelectCondition} to the move if it is only selectable/successful under certain conditions - * @param condition {@linkcode MoveSelectCondition} appends to conditions array a MoveSelectCondition {@linkcode selectableConditions} - * @returns the called object {@linkcode Move} + * Adds a {@linkcode MoveSelectCondition} to the move. It contains the condition if the move should be selectable in the move menu or not + * @param condition {@linkcode MoveSelectCondition} gets pushed into {@linkcode selectableConditions} + * @returns this move {@linkcode Move} */ selectableCondition(condition: MoveSelectCondition): this { if (condition) { @@ -683,7 +683,7 @@ export default class Move implements Localizable { } /** - * Applies each {@linkcode MoveSelectCondition} function of this move to determine if the move can be used selected during {@linkcode CommandPhase} + * Applies each {@linkcode MoveSelectCondition} function of this move to determine if the move can be selected during {@linkcode CommandPhase} * @param user {@linkcode Pokemon} to apply conditions to * @returns boolean: false if any of the apply()'s return false, else true */ @@ -7617,8 +7617,7 @@ export function applyMoveChargeAttrs(attrType: Constructor, user: Poke /** * Base class defining all {@linkcode selectableConditions} - * Is used to add {@linkcode UserMoveConditionFunc} in order to check if condition - * is met and move can be selected/ if move fails + * Is used to add {@linkcode UserMoveConditionFunc} in order to check if move can be selected */ export class MoveSelectCondition { protected func: UserMoveConditionFunc; @@ -7639,13 +7638,10 @@ export class MoveSelectCondition { } /** - * extends {@linkcode MoveSelectCondition} and contains the condition for {@link https://bulbapedia.bulbagarden.net/wiki/Stuff_Cheeks_(move) | Stuff Cheeks} - * success + * extends {@linkcode MoveSelectCondition} and contains the condition for + * {@link https://bulbapedia.bulbagarden.net/wiki/Stuff_Cheeks_(move) | Stuff Cheeks}s success */ export class StuffCheeksCondition extends MoveSelectCondition { - /** - * contains function that checks if the {@linkcode user} is currently holding a berry or not - */ constructor() { super((user, move) => this.selectableCondition(user)); } From 31d7b63f72d94f22c9a90c3e2b6ca01cabfa18e6 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:05:46 +0100 Subject: [PATCH 05/10] Updated documentation in battler-tags.ts --- src/data/battler-tags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 32b5e74661b1..872997f20e3a 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2790,7 +2790,7 @@ export class StuffCheeksTag extends MoveRestrictionBattlerTag { } /** - * This function always returns true since {@linkcode MoveUnselectableTag} is only added if condition {@linkcode MoveSelectCondition} fails + * This function returns true if {@linkcode move} is {@linkcode Moves.STUFF_CHEEKS} * @param move {@linkcode Moves} that is selected * @returns true if the move matches the ID of Stuff Cheeks */ From 55bab5ed63ec321aaf8cd24ea0fd179a647dc01d Mon Sep 17 00:00:00 2001 From: geeil-han Date: Sun, 1 Dec 2024 23:10:58 +0100 Subject: [PATCH 06/10] fix ESLint error --- src/data/move.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 47a84690e2c6..f26febcb0779 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7638,8 +7638,8 @@ export class MoveSelectCondition { } /** - * extends {@linkcode MoveSelectCondition} and contains the condition for - * {@link https://bulbapedia.bulbagarden.net/wiki/Stuff_Cheeks_(move) | Stuff Cheeks}s success + * extends {@linkcode MoveSelectCondition} and contains the condition + * for {@link https://bulbapedia.bulbagarden.net/wiki/Stuff_Cheeks_(move) | Stuff Cheeks}s success */ export class StuffCheeksCondition extends MoveSelectCondition { constructor() { From 97b7e9d9bf6668b5565026beec23161390a2adde Mon Sep 17 00:00:00 2001 From: geeil-han Date: Wed, 4 Dec 2024 20:15:11 +0100 Subject: [PATCH 07/10] changed condition so it works for doubles too --- src/data/move.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/move.ts b/src/data/move.ts index 959b9e525cf3..35873ad8d128 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7772,7 +7772,7 @@ export class StuffCheeksCondition extends MoveSelectCondition { * @returns true if the user is holding a berry, otherwise false */ private selectableCondition(user: Pokemon): boolean { - return user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length > 0; + return user?.getHeldItems()?.filter(m => m instanceof BerryModifier, user.isPlayer())?.length > 0; } /** From 96a4fdbb77f43d6ab2b13169c18b9c3e544ff011 Mon Sep 17 00:00:00 2001 From: geeil-han Date: Wed, 4 Dec 2024 20:35:15 +0100 Subject: [PATCH 08/10] changes to input variables and documentation --- src/data/battler-tags.ts | 6 +++--- src/data/move.ts | 4 ++-- src/phases/command-phase.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 55c68fa15618..b7c44fcdfa34 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2835,10 +2835,10 @@ export class StuffCheeksTag extends MoveRestrictionBattlerTag { // Stuff Cheeks private moveId: Moves = Moves.STUFF_CHEEKS; /** - * This Tag only lasts the turn the {@linkcode MoveUnselectableTag} was added. + * This Tag only lasts the turn the {@linkcode StuffCheeksTag} was added. * @param move {@linkcode Moves} that is selected */ - constructor(move: Moves) { + constructor() { super(BattlerTagType.STUFF_CHEEKS, BattlerTagLapseType.TURN_END, 0, Moves.STUFF_CHEEKS); } @@ -3201,7 +3201,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.PSYCHO_SHIFT: return new PsychoShiftTag(); case BattlerTagType.STUFF_CHEEKS: - return new StuffCheeksTag(sourceMove); + return new StuffCheeksTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index 35873ad8d128..8d56339c627c 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7772,7 +7772,7 @@ export class StuffCheeksCondition extends MoveSelectCondition { * @returns true if the user is holding a berry, otherwise false */ private selectableCondition(user: Pokemon): boolean { - return user?.getHeldItems()?.filter(m => m instanceof BerryModifier, user.isPlayer())?.length > 0; + return user.getHeldItems().filter(m => m instanceof BerryModifier, user.isPlayer()).length > 0; } /** @@ -7791,7 +7791,7 @@ export class StuffCheeksCondition extends MoveSelectCondition { } } -const hasBerryCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length > 0; +const hasBerryCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.getHeldItems().filter(m => m instanceof BerryModifier, user.isPlayer()).length > 0; export class MoveCondition { protected func: MoveConditionFunc; diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 9453c2976153..f033360a5b19 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -149,7 +149,7 @@ export class CommandPhase extends FieldPhase { const move = playerPokemon.getMoveset()[cursor]!; //TODO: is this bang correct? this.scene.ui.setMode(Mode.MESSAGE); - // Decides between a Disabled, not selectable, Not Implemented, or No PP translation message + // Decides between a Disabled, Not Implemented, or No PP translation message const errorMessage = playerPokemon.isMoveRestricted(move.moveId, playerPokemon) ? playerPokemon.getRestrictingTag(move.moveId, playerPokemon)!.selectionDeniedText(playerPokemon, move.moveId) From 85383bfdf5a0193207576c12d455fc21d5628498 Mon Sep 17 00:00:00 2001 From: geeil-han Date: Thu, 5 Dec 2024 02:23:12 +0100 Subject: [PATCH 09/10] changed documentation in tests --- src/test/moves/stuff_cheeks.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/moves/stuff_cheeks.test.ts b/src/test/moves/stuff_cheeks.test.ts index ce1d5f05d166..676e7f7af418 100644 --- a/src/test/moves/stuff_cheeks.test.ts +++ b/src/test/moves/stuff_cheeks.test.ts @@ -10,7 +10,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -describe("Abilities - Stuff Cheeks", () => { +describe("Moves - Stuff Cheeks", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -163,7 +163,7 @@ describe("Abilities - Stuff Cheeks", () => { // Can be enabled when Knock off correctly knocks off the held berry it.todo("should fail when used after berries getting knocked off", async () => { - game.override + game.override.startingWave(5) .startingHeldItems([ { name: "BERRY", type: BerryType.SITRUS, count: 1 }, ]) From 58e80b4e49118051cfadeef05560893744527c90 Mon Sep 17 00:00:00 2001 From: geeil-han Date: Thu, 5 Dec 2024 19:32:54 +0100 Subject: [PATCH 10/10] changed test so knock off interaction with stuff cheeks test works --- src/test/moves/stuff_cheeks.test.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/test/moves/stuff_cheeks.test.ts b/src/test/moves/stuff_cheeks.test.ts index 676e7f7af418..4639bda8f8b2 100644 --- a/src/test/moves/stuff_cheeks.test.ts +++ b/src/test/moves/stuff_cheeks.test.ts @@ -161,24 +161,26 @@ describe("Moves - Stuff Cheeks", () => { expect(getHeldItemCount(player)).toBe(3); }); - // Can be enabled when Knock off correctly knocks off the held berry - it.todo("should fail when used after berries getting knocked off", async () => { - game.override.startingWave(5) - .startingHeldItems([ + it("from enemy should fail when player knocks off enemy berry before", async () => { + game.override + .enemyHeldItems([ { name: "BERRY", type: BerryType.SITRUS, count: 1 }, ]) - .enemyMoveset(Moves.KNOCK_OFF); + .enemyMoveset(Moves.STUFF_CHEEKS) + .moveset(Moves.KNOCK_OFF) + /** This is set so the enemy does not get oneshot by Knock Off */ + .enemyLevel(100); await game.classicMode.startBattle([ Species.BULBASAUR ]); - const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; - game.move.select(Moves.STUFF_CHEEKS); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.move.select(Moves.KNOCK_OFF); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.toNextTurn(); - expect(getHeldItemCount(player)).toBe(0); - expect(player.getStatStage(Stat.DEF)).toBe(0); + expect(getHeldItemCount(enemy)).toBe(0); + expect(enemy.getStatStage(Stat.DEF)).toBe(0); }); });