Compare commits

...

13 Commits

Author SHA1 Message Date
Sirz Benjie
a6573c6101
Merge 855c956aad into 1ff2701964 2025-06-19 22:29:37 -04:00
lnuvy
1ff2701964
[Bug] Fix when using arrow keys in Pokédex after catching a Pokémon from mystery event (#6000)
fix: wrap setOverlayMode args in array to mystery-encounter

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-06-19 20:45:54 -04:00
Bertie690
1e306e25b5
[Move] Fixed Chilly Reception displaying message when used virtually
https://github.com/pagefaultgames/pokerogue/pull/5843

* Fixed Chilly Reception displaying message when used virtually

* Fixed lack of message causing Chilly Reception to fail

* Fixed tests

* Reverted bool change + fixed test

* Fixed test
2025-06-19 17:14:05 -07:00
Madmadness65
43aa772603
[UI/UX] Add Pokémon category flavor text to Pokédex (#5957)
* Add Pokémon category flavor text to Pokédex

* Append `_category` to locale entry
2025-06-20 00:04:57 +00:00
Sirz Benjie
855c956aad
Update with Bertie's comments
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
2025-06-18 15:07:58 -06:00
Sirz Benjie
ecc175f176
Explicitly check against Moves.NONE
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
2025-06-18 15:05:42 -06:00
Sirz Benjie
648a565eea
Improve canUse computation 2025-06-18 15:05:41 -06:00
Sirz Benjie
f856262168
Fix improperly named computeMoveId method 2025-06-18 15:05:41 -06:00
Sirz Benjie
f678966149
Add overload for handle command 2025-06-18 15:05:38 -06:00
Sirz Benjie
3497f93141
Minor touchups 2025-06-18 15:04:37 -06:00
Sirz Benjie
ab94e82163
Breakup commandPhase#start
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
2025-06-18 15:04:11 -06:00
Sirz Benjie
adae272e18
Breakup run and pokemon commands 2025-06-18 15:00:55 -06:00
Sirz Benjie
200db0c435
breakup fight and ball commands into their own methods 2025-06-18 14:41:22 -06:00
8 changed files with 644 additions and 448 deletions

View File

@ -93,6 +93,10 @@ import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap
import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils";
/**
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
* Conventionally returns `true` for success and `false` for failure.
*/
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
@ -1390,18 +1394,31 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
}
}
/**
* Attribute to display a message before a move is executed.
*/
export class PreMoveMessageAttr extends MoveAttr {
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
/** The message to display or a function returning one */
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined);
/**
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
* @param message - The message to display before move use, either as a string or a function producing one.
* @remarks
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed
* (though the move will still succeed).
*/
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) {
super();
this.message = message;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const message = typeof this.message === "string"
? this.message as string
: this.message(user, target, move);
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
const message = typeof this.message === "function"
? this.message(user, target, move)
: this.message;
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
if (message) {
globalScene.phaseManager.queueMessage(message, 500);
return true;
@ -11299,7 +11316,11 @@ export function initMoves() {
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
.condition(failIfLastInPartyCondition),
new SelfStatusMove(MoveId.CHILLY_RECEPTION, PokemonType.ICE, -1, 10, -1, 0, 9)
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(PreMoveMessageAttr, (user, _target, _move) =>
// Don't display text if current move phase is follow up (ie move called indirectly)
isVirtual((globalScene.phaseManager.getCurrentPhase() as MovePhase).useMode)
? ""
: i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(ChillyReceptionAttr, true),
new SelfStatusMove(MoveId.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)

View File

@ -751,7 +751,7 @@ export async function catchPokemon(
UiMode.POKEDEX_PAGE,
pokemon.species,
pokemon.formIndex,
attributes,
[attributes],
null,
() => {
globalScene.ui.setMode(UiMode.MESSAGE).then(() => {

View File

@ -764,7 +764,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
readonly subLegendary: boolean;
readonly legendary: boolean;
readonly mythical: boolean;
readonly species: string;
public category: string;
readonly growthRate: GrowthRate;
/** The chance (as a decimal) for this Species to be male, or `null` for genderless species */
readonly malePercent: number | null;
@ -778,7 +778,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
subLegendary: boolean,
legendary: boolean,
mythical: boolean,
species: string,
category: string,
type1: PokemonType,
type2: PokemonType | null,
height: number,
@ -829,7 +829,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
this.subLegendary = subLegendary;
this.legendary = legendary;
this.mythical = mythical;
this.species = species;
this.category = category;
this.growthRate = growthRate;
this.malePercent = malePercent;
this.genderDiffs = genderDiffs;
@ -968,6 +968,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
localize(): void {
this.name = i18next.t(`pokemon:${SpeciesId[this.speciesId].toLowerCase()}`);
this.category = i18next.t(`pokemonCategory:${SpeciesId[this.speciesId].toLowerCase()}_category`);
}
getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): SpeciesId {

View File

@ -1,7 +1,6 @@
import { globalScene } from "#app/global-scene";
import type { TurnCommand } from "#app/battle";
import { BattleType } from "#enums/battle-type";
import type { EncoreTag } from "#app/data/battler-tags";
import { TrappedTag } from "#app/data/battler-tags";
import type { MoveTargetSet } from "#app/data/moves/move";
import { getMoveTargets } from "#app/data/moves/move-utils";
@ -19,7 +18,6 @@ import { UiMode } from "#enums/ui-mode";
import i18next from "i18next";
import { FieldPhase } from "./field-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { isNullOrUndefined } from "#app/utils/common";
import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
@ -28,17 +26,24 @@ export class CommandPhase extends FieldPhase {
public readonly phaseName = "CommandPhase";
protected fieldIndex: number;
/**
* Whether the command phase is handling a switch command
*/
private isSwitch = false;
constructor(fieldIndex: number) {
super();
this.fieldIndex = fieldIndex;
}
start() {
super.start();
globalScene.updateGameInfo();
/**
* Resets the cursor to the position of {@linkcode Command.FIGHT} if any of the following are true
* - The setting to remember the last action is not enabled
* - This is the first turn of a mystery encounter, trainer battle, or the END biome
* - The cursor is currently on the POKEMON command
*/
private resetCursorIfNeeded() {
const commandUiHandler = globalScene.ui.handlers[UiMode.COMMAND];
// If one of these conditions is true, we always reset the cursor to Command.FIGHT
@ -47,33 +52,42 @@ export class CommandPhase extends FieldPhase {
globalScene.currentBattle.battleType === BattleType.TRAINER ||
globalScene.arena.biomeType === BiomeId.END;
if (commandUiHandler) {
if (
(globalScene.currentBattle.turn === 1 && (!globalScene.commandCursorMemory || cursorResetEvent)) ||
commandUiHandler.getCursor() === Command.POKEMON
) {
commandUiHandler.setCursor(Command.FIGHT);
} else {
commandUiHandler.setCursor(commandUiHandler.getCursor());
}
if (
(commandUiHandler &&
globalScene.currentBattle.turn === 1 &&
(!globalScene.commandCursorMemory || cursorResetEvent)) ||
commandUiHandler.getCursor() === Command.POKEMON
) {
commandUiHandler.setCursor(Command.FIGHT);
}
}
/**
* Submethod of {@linkcode start} that validates field index logic for nonzero field indices.
* Must only be called if the field index is nonzero.
*/
private handleFieldIndexLogic() {
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching
// TODO: Prevent this from happening in the first place
if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) {
this.fieldIndex = FieldPosition.CENTER;
return;
}
if (this.fieldIndex) {
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching
if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) {
this.fieldIndex = FieldPosition.CENTER;
} else {
const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1];
if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) {
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: allyCommand?.command,
skip: true,
};
}
}
const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1];
if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) {
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: allyCommand?.command,
skip: true,
};
}
}
/** Submethod of {@linkcode start} that sets the turn command to skip if this pokemon is commanding its ally
* via {@linkcode Abilities.COMMANDER}.
*/
private checkCommander() {
// If the Pokemon has applied Commander's effects to its ally, skip this command
if (
globalScene.currentBattle?.double &&
@ -85,377 +99,488 @@ export class CommandPhase extends FieldPhase {
skip: true,
};
}
}
// Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP.
const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag;
if (encoreTag) {
this.getPokemon().lapseTag(BattlerTagType.ENCORE);
/**
* Clear out all unusable moves in front of the currently acting pokemon's move queue.
* TODO: Refactor move queue handling to ensure that this method is not necessary.
*/
private clearUnusuableMoves() {
const playerPokemon = this.getPokemon();
const moveQueue = playerPokemon.getMoveQueue();
if (moveQueue.length === 0) {
return;
}
let entriesToDelete = 0;
const moveset = playerPokemon.getMoveset();
for (const queuedMove of moveQueue) {
const movesetQueuedMove = moveset.find(m => m.moveId === queuedMove.move);
if (
queuedMove.move !== MoveId.NONE &&
!isVirtual(queuedMove.useMode) &&
!movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))
) {
entriesToDelete++;
} else {
break;
}
}
if (entriesToDelete) {
moveQueue.splice(0, entriesToDelete);
}
}
/**
* Attempt to execute the first usable move in this Pokemon's move queue
* @returns Whether a queued move was successfully set to be executed.
*/
private tryExecuteQueuedMove(): boolean {
this.clearUnusuableMoves();
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
const moveQueue = playerPokemon.getMoveQueue();
if (moveQueue.length === 0) {
return false;
}
const queuedMove = moveQueue[0];
if (queuedMove.move === MoveId.NONE) {
this.handleCommand(Command.FIGHT, -1);
return true;
}
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
if (!isVirtual(queuedMove.useMode) && moveIndex === -1) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
} else {
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove);
}
return true;
}
start() {
super.start();
globalScene.updateGameInfo();
this.resetCursorIfNeeded();
if (this.fieldIndex) {
this.handleFieldIndexLogic();
}
this.checkCommander();
const playerPokemon = this.getPokemon();
// Note: It is OK to call this if the target is not under the effect of encore; it will simply do nothing.
playerPokemon.lapseTag(BattlerTagType.ENCORE);
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
return this.end();
}
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
const moveQueue = playerPokemon.getMoveQueue();
while (
moveQueue.length &&
moveQueue[0] &&
moveQueue[0].move &&
!isVirtual(moveQueue[0].useMode) &&
(!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) ||
!playerPokemon
.getMoveset()
[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(
playerPokemon,
isIgnorePP(moveQueue[0].useMode),
))
) {
moveQueue.shift();
if (this.tryExecuteQueuedMove()) {
return;
}
// TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured
if (moveQueue.length > 0) {
const queuedMove = moveQueue[0];
if (!queuedMove.move) {
this.handleCommand(Command.FIGHT, -1, MoveUseMode.NORMAL);
} else {
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
if (
(moveIndex > -1 &&
playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))) ||
isVirtual(queuedMove.useMode)
) {
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove);
} else {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
}
if (
globalScene.currentBattle.isBattleMysteryEncounter() &&
globalScene.currentBattle.mysteryEncounter?.skipToFightInput
) {
globalScene.ui.clearText();
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
} else {
if (
globalScene.currentBattle.isBattleMysteryEncounter() &&
globalScene.currentBattle.mysteryEncounter?.skipToFightInput
) {
globalScene.ui.clearText();
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
} else {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
}
/**
* TODO: Remove `args` and clean this thing up
* Code will need to be copied over from pkty except replacing the `virtual` and `ignorePP` args with a corresponding `MoveUseMode`.
*
* @param user - The pokemon using the move
* @param cursor - The index of the move in the moveset
*/
handleCommand(command: Command, cursor: number, ...args: any[]): boolean {
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
private queueFightErrorMessage(user: PlayerPokemon, cursor: number) {
const move = user.getMoveset()[cursor];
globalScene.ui.setMode(UiMode.MESSAGE);
// Decides between a Disabled, Not Implemented, or No PP translation message
const errorMessage = user.isMoveRestricted(move.moveId, user)
? user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId)
: move.getName().endsWith(" (N)")
? "battle:moveNotImplemented"
: "battle:moveNoPP";
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
globalScene.ui.showText(
i18next.t(errorMessage, { moveName: moveName }),
null,
() => {
globalScene.ui.clearText();
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
},
null,
true,
);
}
/**
* Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase
* based on the move passed in or the cursor.
*
* Does not check if the move is usable or not, that should be handled by the caller.
*/
private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId {
return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE);
}
/**
* Process the logic for executing a fight-related command
*
* @remarks
* - Validates whether the move can be used, using struggle if not
* - Constructs the turn command and inserts it into the battle's turn commands
*
* @param command - The command to handle (FIGHT or TERA)
* @param cursor - The index that the cursor is placed on, or -1 if no move can be selected.
* @param ignorePP - Whether to ignore PP when checking if the move can be used.
* @param move - The move to force the command to use, if any.
*/
private handleFightCommand(
command: Command.FIGHT | Command.TERA,
cursor: number,
useMode: MoveUseMode = MoveUseMode.NORMAL,
move?: TurnMove,
): boolean {
const playerPokemon = this.getPokemon();
const ignorePP = isIgnorePP(useMode);
let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP);
// Ternary here ensures we don't compute struggle conditions unless necessary
const useStruggle = canUse
? false
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon));
canUse ||= useStruggle;
if (!canUse) {
this.queueFightErrorMessage(playerPokemon, cursor);
return false;
}
const moveId = useStruggle ? MoveId.STRUGGLE : this.computeMoveId(playerPokemon, cursor, move);
const turnCommand: TurnCommand = {
command: Command.FIGHT,
cursor: cursor,
move: { move: moveId, targets: [], useMode },
args: [useMode, move],
};
const preTurnCommand: TurnCommand = {
command: command,
targets: [this.fieldIndex],
skip: command === Command.FIGHT,
};
const moveTargets: MoveTargetSet =
move === undefined
? getMoveTargets(playerPokemon, moveId)
: {
targets: move.targets,
multiple: move.targets.length > 1,
};
if (moveId === MoveId.NONE) {
turnCommand.targets = [this.fieldIndex];
}
console.log(moveTargets, getPokemonNameWithAffix(playerPokemon));
if (moveTargets.targets.length > 1 && moveTargets.multiple) {
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
}
if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) {
turnCommand.move.targets = moveTargets.targets;
} else if (
turnCommand.move &&
playerPokemon.getTag(BattlerTagType.CHARGING) &&
playerPokemon.getMoveQueue().length >= 1
) {
turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets;
} else {
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
}
globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand;
globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
return true;
}
/**
* Set the mode in preparation to show the text, and then show the text.
* Only works for parameterless i18next keys.
* @param key - The i18next key for the text to show
*/
private queueShowText(key: string) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t(key),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
}
/**
* Helper method for {@linkcode handleBallCommand} that checks if the pokeball can be thrown.
*
* The pokeball may not be thrown if:
* - It is a trainer battle
* - The biome is {@linkcode Biome.END} and it is not classic mode
* - The biome is {@linkcode Biome.END} and the fresh start challenge is active
* - The biome is {@linkcode Biome.END} and the player has not either caught the target before or caught all but one starter
* - The player is in a mystery encounter that disallows catching the pokemon
* @returns Whether a pokeball can be thrown
*
*/
private checkCanUseBall(): boolean {
if (
globalScene.arena.biomeType === BiomeId.END &&
(!globalScene.gameMode.isClassic ||
globalScene.gameMode.isFreshStartChallenge() ||
(globalScene
.getEnemyField()
.some(p => p.isActive() && !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) &&
globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1))
) {
this.queueShowText("battle:noPokeballForce");
} else if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
this.queueShowText("battle:noPokeballTrainer");
} else if (
globalScene.currentBattle.isBattleMysteryEncounter() &&
!globalScene.currentBattle.mysteryEncounter!.catchAllowed
) {
this.queueShowText("battle:noPokeballMysteryEncounter");
} else {
return true;
}
return false;
}
/**
* Helper method for {@linkcode handleCommand} that handles the logic when the selected command is to use a pokeball.
*
* @param cursor - The index of the pokeball to use
* @returns Whether the command was successfully initiated
*/
private handleBallCommand(cursor: number): boolean {
const targets = globalScene
.getEnemyField()
.filter(p => p.isActive(true))
.map(p => p.getBattlerIndex());
if (targets.length > 1) {
this.queueShowText("battle:noPokeballMulti");
return false;
}
if (!this.checkCanUseBall()) {
return false;
}
if (cursor < 5) {
const targetPokemon = globalScene.getEnemyPokemon();
if (
targetPokemon?.isBoss() &&
targetPokemon?.bossSegmentIndex >= 1 &&
// TODO: Decouple this hardcoded exception for wonder guard and just check the target...
!targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) &&
cursor < PokeballType.MASTER_BALL
) {
this.queueShowText("battle:noPokeballStrong");
return false;
}
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: Command.BALL,
cursor: cursor,
};
globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets;
if (this.fieldIndex) {
globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
return true;
}
return false;
}
/**
* Helper method to handle the logic for effects that prevent the pokemon from leaving the field
* due to trapping abilities or effects.
*
* This method queues the proper messages in the case of trapping abilities or effects
*
* @returns Whether the pokemon is currently trapped
*/
private handleTrap(): boolean {
const playerPokemon = this.getPokemon();
const trappedAbMessages: string[] = [];
const isSwitch = this.isSwitch;
if (!playerPokemon.isTrapped(trappedAbMessages)) {
return false;
}
if (trappedAbMessages.length > 0) {
if (isSwitch) {
globalScene.ui.setMode(UiMode.MESSAGE);
}
globalScene.ui.showText(
trappedAbMessages[0],
null,
() => {
globalScene.ui.showText("", 0);
if (isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
null,
true,
);
} else {
const trapTag = playerPokemon.getTag(TrappedTag);
const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER);
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
}
if (trapTag) {
this.showNoEscapeText(trapTag, false);
} else if (fairyLockTag) {
this.showNoEscapeText(fairyLockTag, false);
}
}
return true;
}
/**
* Common helper method that attempts to have the pokemon leave the field.
* Checks for trapping abilities and effects.
*
* @param cursor - The index of the option that the cursor is on
* @param isBatonSwitch - Whether the switch command is switching via the Baton item
* @returns whether the pokemon is able to leave the field, indicating the command phase should end
*/
private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean {
const currentBattle = globalScene.currentBattle;
if (isBatonSwitch || !this.handleTrap()) {
currentBattle.turnCommands[this.fieldIndex] = this.isSwitch
? {
command: Command.POKEMON,
cursor: cursor,
args: [isBatonSwitch],
}
: {
command: Command.RUN,
};
if (!this.isSwitch && this.fieldIndex) {
currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
return true;
}
return false;
}
private handleRunCommand(): boolean {
const { currentBattle, arena } = globalScene;
const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed ?? true;
if (arena.biomeType === BiomeId.END || !mysteryEncounterFleeAllowed) {
this.queueShowText("battle:noEscapeForce");
return false;
}
if (
currentBattle.battleType === BattleType.TRAINER ||
currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE
) {
this.queueShowText("battle:noEscapeTrainer");
return false;
}
const success = this.tryLeaveField();
return success;
}
/**
* Show a message indicating that the pokemon cannot escape, and then return to the command phase.
*/
private showNoEscapeText(tag: any, isSwitch: boolean): void {
globalScene.ui.showText(
i18next.t("battle:noEscapePokemon", {
pokemonName:
tag.sourceId && globalScene.getPokemonById(tag.sourceId)
? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!)
: "",
moveName: tag.getMoveName(),
escapeVerb: i18next.t(isSwitch ? "battle:escapeVerbSwitch" : "battle:escapeVerbFlee"),
}),
null,
() => {
globalScene.ui.showText("", 0);
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
null,
true,
);
}
// Overloads for handleCommand to provide a more specific signature for the different options
handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean;
handleCommand(command: Command.BALL, cursor: number): boolean;
handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean;
handleCommand(command: Command.RUN, cursor: number): boolean;
handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean;
/**
* Process the command phase logic based on the selected command
*
* @param command - The kind of command to handle
* @param cursor - The index of option that the cursor is on, or -1 if no option is selected
* @param useMode - The mode to use for the move, if applicable. For switches, a boolean that specifies whether the switch is a Baton switch.
* @param move - For {@linkcode Command.FIGHT}, the move to use
*/
handleCommand(command: Command, cursor: number, useMode: boolean | MoveUseMode = false, move?: TurnMove): boolean {
let success = false;
switch (command) {
// TODO: We don't need 2 args for this - moveUseMode is carried over from queuedMove
case Command.TERA:
case Command.FIGHT: {
let useStruggle = false;
const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined;
if (
cursor === -1 ||
playerPokemon.trySelectMove(cursor, isIgnorePP(args[0] as MoveUseMode)) ||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)
) {
let moveId: MoveId;
if (useStruggle) {
moveId = MoveId.STRUGGLE;
} else if (turnMove !== undefined) {
moveId = turnMove.move;
} else if (cursor > -1) {
moveId = playerPokemon.getMoveset()[cursor].moveId;
} else {
moveId = MoveId.NONE;
}
const turnCommand: TurnCommand = {
command: Command.FIGHT,
cursor: cursor,
move: { move: moveId, targets: [], useMode: args[0] },
args: args,
};
const preTurnCommand: TurnCommand = {
command: command,
targets: [this.fieldIndex],
skip: command === Command.FIGHT,
};
const moveTargets: MoveTargetSet =
turnMove === undefined
? getMoveTargets(playerPokemon, moveId)
: {
targets: turnMove.targets,
multiple: turnMove.targets.length > 1,
};
if (!moveId) {
turnCommand.targets = [this.fieldIndex];
}
console.log(moveTargets, getPokemonNameWithAffix(playerPokemon));
if (moveTargets.targets.length > 1 && moveTargets.multiple) {
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
}
if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) {
turnCommand.move.targets = moveTargets.targets;
} else if (
turnCommand.move &&
playerPokemon.getTag(BattlerTagType.CHARGING) &&
playerPokemon.getMoveQueue().length >= 1
) {
turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets;
} else {
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
}
globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand;
globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
success = true;
} else if (cursor < playerPokemon.getMoveset().length) {
const move = playerPokemon.getMoveset()[cursor];
globalScene.ui.setMode(UiMode.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)
: move.getName().endsWith(" (N)")
? "battle:moveNotImplemented"
: "battle:moveNoPP";
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
globalScene.ui.showText(
i18next.t(errorMessage, { moveName: moveName }),
null,
() => {
globalScene.ui.clearText();
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
},
null,
true,
);
}
case Command.FIGHT:
success = this.handleFightCommand(command, cursor, typeof useMode === "boolean" ? undefined : useMode, move);
break;
}
case Command.BALL: {
const notInDex =
globalScene
.getEnemyField()
.filter(p => p.isActive(true))
.some(p => !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) &&
globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1;
if (
globalScene.arena.biomeType === BiomeId.END &&
(!globalScene.gameMode.isClassic || globalScene.gameMode.isFreshStartChallenge() || notInDex)
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballForce"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballTrainer"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else if (
globalScene.currentBattle.isBattleMysteryEncounter() &&
!globalScene.currentBattle.mysteryEncounter!.catchAllowed
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballMysteryEncounter"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else {
const targets = globalScene
.getEnemyField()
.filter(p => p.isActive(true))
.map(p => p.getBattlerIndex());
if (targets.length > 1) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballMulti"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else if (cursor < 5) {
const targetPokemon = globalScene.getEnemyField().find(p => p.isActive(true));
if (
targetPokemon?.isBoss() &&
targetPokemon?.bossSegmentIndex >= 1 &&
!targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) &&
cursor < PokeballType.MASTER_BALL
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noPokeballStrong"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else {
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: Command.BALL,
cursor: cursor,
};
globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets;
if (this.fieldIndex) {
globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
success = true;
}
}
}
case Command.BALL:
success = this.handleBallCommand(cursor);
break;
}
case Command.POKEMON:
case Command.RUN: {
const isSwitch = command === Command.POKEMON;
const { currentBattle, arena } = globalScene;
const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed;
if (
!isSwitch &&
(arena.biomeType === BiomeId.END ||
(!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed))
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noEscapeForce"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else if (
!isSwitch &&
(currentBattle.battleType === BattleType.TRAINER ||
currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)
) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
globalScene.ui.showText(
i18next.t("battle:noEscapeTrainer"),
null,
() => {
globalScene.ui.showText("", 0);
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
},
null,
true,
);
} else {
const batonPass = isSwitch && (args[0] as boolean);
const trappedAbMessages: string[] = [];
if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) {
currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
success = true;
if (!isSwitch && this.fieldIndex) {
currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
} else if (trappedAbMessages.length > 0) {
if (!isSwitch) {
globalScene.ui.setMode(UiMode.MESSAGE);
}
globalScene.ui.showText(
trappedAbMessages[0],
null,
() => {
globalScene.ui.showText("", 0);
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
null,
true,
);
} else {
const trapTag = playerPokemon.getTag(TrappedTag);
const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER);
if (!trapTag && !fairyLockTag) {
i18next.t(`battle:noEscape${isSwitch ? "Switch" : "Flee"}`);
break;
}
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
globalScene.ui.setMode(UiMode.MESSAGE);
}
const showNoEscapeText = (tag: any) => {
globalScene.ui.showText(
i18next.t("battle:noEscapePokemon", {
pokemonName:
tag.sourceId && globalScene.getPokemonById(tag.sourceId)
? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!)
: "",
moveName: tag.getMoveName(),
escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee"),
}),
null,
() => {
globalScene.ui.showText("", 0);
if (!isSwitch) {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
}
},
null,
true,
);
};
if (trapTag) {
showNoEscapeText(trapTag);
} else if (fairyLockTag) {
showNoEscapeText(fairyLockTag);
}
}
}
this.isSwitch = true;
success = this.tryLeaveField(cursor, typeof useMode === "boolean" ? useMode : undefined);
this.isSwitch = false;
break;
}
case Command.RUN:
success = this.handleRunCommand();
}
if (success) {

View File

@ -668,6 +668,9 @@ export class MovePhase extends BattlePhase {
}),
500,
);
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
// TODO: This assumes single target for message funcs - is this sustainable?
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
}

View File

@ -245,6 +245,7 @@ export async function initI18n(): Promise<void> {
"pokeball",
"pokedexUiHandler",
"pokemon",
"pokemonCategory",
"pokemonEvolutions",
"pokemonForm",
"pokemonInfo",

View File

@ -174,6 +174,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
private pokemonCaughtHatchedContainer: Phaser.GameObjects.Container;
private pokemonCaughtCountText: Phaser.GameObjects.Text;
private pokemonFormText: Phaser.GameObjects.Text;
private pokemonCategoryText: Phaser.GameObjects.Text;
private pokemonHatchedIcon: Phaser.GameObjects.Sprite;
private pokemonHatchedCountText: Phaser.GameObjects.Text;
private pokemonShinyIcons: Phaser.GameObjects.Sprite[];
@ -409,6 +410,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonFormText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonFormText);
this.pokemonCategoryText = addTextObject(100, 18, "Category", TextStyle.WINDOW_ALT, {
fontSize: "42px",
});
this.pokemonCategoryText.setOrigin(1, 0);
this.starterSelectContainer.add(this.pokemonCategoryText);
this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25);
this.pokemonCaughtHatchedContainer.setScale(0.5);
this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer);
@ -2354,6 +2361,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(true);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.pokemonCategoryText.setVisible(false);
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true);
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
@ -2382,6 +2390,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.pokemonCategoryText.setVisible(false);
this.setSpeciesDetails(species!, {
// TODO: is this bang correct?
@ -2534,6 +2543,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonNameText.setText(species ? "???" : "");
}
// Setting the category
if (isFormCaught) {
this.pokemonCategoryText.setText(species.category);
} else {
this.pokemonCategoryText.setText("");
}
// Setting tint of the sprite
if (isFormCaught) {
this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => {

View File

@ -1,11 +1,14 @@
import { AbilityId } from "#enums/ability-id";
import { RandomMoveAttr } from "#app/data/moves/move";
import { MoveResult } from "#enums/move-result";
import { getPokemonNameWithAffix } from "#app/messages";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { AbilityId } from "#app/enums/ability-id";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager";
import i18next from "i18next";
import Phaser from "phaser";
//import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Chilly Reception", () => {
let phaserGame: Phaser.Game;
@ -25,95 +28,121 @@ describe("Moves - Chilly Reception", () => {
game = new GameManager(phaserGame);
game.override
.battleStyle("single")
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE])
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE, MoveId.SPLASH, MoveId.METRONOME])
.enemyMoveset(MoveId.SPLASH)
.enemyAbility(AbilityId.BALL_FETCH)
.ability(AbilityId.BALL_FETCH);
});
it("should still change the weather if user can't switch out", async () => {
it("should display message before use, switch the user out and change the weather to snow", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
it("should still change weather if user can't switch out", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING]);
game.move.select(MoveId.CHILLY_RECEPTION);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
});
it("should switch out even if it's snowing", async () => {
it("should still switch out even if weather cannot be changed", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
// first turn set up snow with snowscape, try chilly reception on second turn
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.SNOWSCAPE);
await game.phaseInterceptor.to("BerryPhase", false);
await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
// TODO: Uncomment lines once wimp out PR fixes force switches to not reset summon data immediately
// await game.phaseInterceptor.to("SwitchSummonPhase", false);
// expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
});
it("happy case - switch out and weather changes", async () => {
// Source: https://replay.pokemonshowdown.com/gen9ou-2367532550
it("should fail (while still displaying message) if neither weather change nor switch out succeeds", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING]);
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW);
const slowking = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SNOWSCAPE);
await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()).toBe(slowking);
expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
it("should succeed without message if called indirectly", async () => {
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.CHILLY_RECEPTION);
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.METRONOME);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH);
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.textInterceptor.logs).not.toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
// enemy uses another move and weather doesn't change
it("check case - enemy not selecting chilly reception doesn't change weather ", async () => {
game.override.battleStyle("single").enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]).moveset(MoveId.SPLASH);
// Bugcheck test for enemy AI bug
it("check case - enemy not selecting chilly reception doesn't change weather", async () => {
game.override.enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]);
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
game.move.select(MoveId.SPLASH);
await game.move.selectEnemyMove(MoveId.TACKLE);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(undefined);
});
it("enemy trainer - expected behavior ", async () => {
game.override
.battleStyle("single")
.startingWave(8)
.enemyMoveset(MoveId.CHILLY_RECEPTION)
.enemySpecies(SpeciesId.MAGIKARP)
.moveset([MoveId.SPLASH, MoveId.THUNDERBOLT]);
await game.classicMode.startBattle([SpeciesId.JOLTEON]);
const RIVAL_MAGIKARP1 = game.scene.getEnemyPokemon()?.id;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_MAGIKARP1);
await game.phaseInterceptor.to("TurnInitPhase", false);
game.move.select(MoveId.SPLASH);
// second chilly reception should still switch out
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
expect(game.scene.getEnemyPokemon()?.id === RIVAL_MAGIKARP1);
game.move.select(MoveId.THUNDERBOLT);
// enemy chilly recep move should fail: it's snowing and no option to switch out
// no crashing
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.arena.weather?.weatherType).toBeUndefined();
});
});