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 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<MoveAttr>, 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)
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -136,10 +136,11 @@ 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.isSelectable(playerPokemon) ? "battle:moveCannotBeSelected"
|
||||
: move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP";
|
||||
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
|
||||
|
||||
|
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