mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-30 12:12:28 +02:00
Added full implementation of Stuff Cheeks
This commit is contained in:
parent
ae8efeedf8
commit
e86bdf9eb8
@ -142,6 +142,8 @@ export default class Move implements Localizable {
|
|||||||
public generation: number;
|
public generation: number;
|
||||||
public attrs: MoveAttr[] = [];
|
public attrs: MoveAttr[] = [];
|
||||||
private conditions: MoveCondition[] = [];
|
private conditions: MoveCondition[] = [];
|
||||||
|
/** contains conditions if move is selectable and should fail or not */
|
||||||
|
private selectableConditions: MoveSelectCondition[] = [];
|
||||||
/** The move's {@linkcode MoveFlags} */
|
/** The move's {@linkcode MoveFlags} */
|
||||||
private flags: number = 0;
|
private flags: number = 0;
|
||||||
private nameAppend: string = "";
|
private nameAppend: string = "";
|
||||||
@ -374,6 +376,20 @@ export default class Move implements Localizable {
|
|||||||
return this;
|
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.
|
* Internal dev flag for documenting edge cases. When using this, please document the known edge case.
|
||||||
* @returns the called object {@linkcode Move}
|
* @returns the called object {@linkcode Move}
|
||||||
@ -667,6 +683,21 @@ export default class Move implements Localizable {
|
|||||||
return true;
|
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)
|
* 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
|
* @param user {@linkcode Pokemon} using the move
|
||||||
@ -7496,6 +7527,44 @@ export function applyMoveChargeAttrs(attrType: Constructor<MoveAttr>, user: Poke
|
|||||||
return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
|
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 {
|
export class MoveCondition {
|
||||||
protected func: MoveConditionFunc;
|
protected func: MoveConditionFunc;
|
||||||
|
|
||||||
@ -9989,11 +10058,7 @@ export function initMoves() {
|
|||||||
new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8)
|
new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8)
|
||||||
.attr(EatBerryAttr)
|
.attr(EatBerryAttr)
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
|
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
|
||||||
.condition((user) => {
|
.selectableCondition(new StuffCheeksCondition()),
|
||||||
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
|
|
||||||
new SelfStatusMove(Moves.NO_RETREAT, Type.FIGHTING, -1, 5, -1, 0, 8)
|
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(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
|
||||||
|
@ -5355,6 +5355,10 @@ export class PokemonMove {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.isSelectable(pokemon)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.getMove().name.endsWith(" (N)")) {
|
if (this.getMove().name.endsWith(" (N)")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -5362,6 +5366,20 @@ export class PokemonMove {
|
|||||||
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1);
|
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 {
|
getMove(): Move {
|
||||||
return allMoves[this.moveId];
|
return allMoves[this.moveId];
|
||||||
}
|
}
|
||||||
|
@ -136,11 +136,12 @@ export class CommandPhase extends FieldPhase {
|
|||||||
const move = playerPokemon.getMoveset()[cursor]!; //TODO: is this bang correct?
|
const move = playerPokemon.getMoveset()[cursor]!; //TODO: is this bang correct?
|
||||||
this.scene.ui.setMode(Mode.MESSAGE);
|
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 =
|
const errorMessage =
|
||||||
playerPokemon.isMoveRestricted(move.moveId, playerPokemon)
|
playerPokemon.isMoveRestricted(move.moveId, playerPokemon)
|
||||||
? playerPokemon.getRestrictingTag(move.moveId, playerPokemon)!.selectionDeniedText(playerPokemon, move.moveId)
|
? 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
|
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
|
||||||
|
|
||||||
this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => {
|
this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => {
|
||||||
|
184
src/test/moves/stuff_cheeks.test.ts
Normal file
184
src/test/moves/stuff_cheeks.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user