Fixed bugs, split up status code, re-added required Rest parameter

This commit is contained in:
Bertie690 2025-05-26 09:48:45 -04:00
parent 9bfb1bba88
commit db927e8adb
10 changed files with 203 additions and 156 deletions

View File

@ -856,6 +856,7 @@ class ToxicSpikesTag extends ArenaTrapTag {
this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC,
true,
null,
0,
this.getMoveName(),
);
}

View File

@ -1873,11 +1873,11 @@ export class HealAttr extends MoveEffectAttr {
/** Should an animation be shown? */
private showAnim: boolean;
constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) {
super(selfTarget === undefined || selfTarget);
constructor(healRatio = 1, showAnim = false, selfTarget = true) {
super(selfTarget);
this.healRatio = healRatio || 1;
this.showAnim = !!showAnim;
this.healRatio = healRatio;
this.showAnim = showAnim;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -2440,9 +2440,8 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
export class StatusEffectAttr extends MoveEffectAttr {
public effect: StatusEffect;
private overrideStatus: boolean;
constructor(effect: StatusEffect, selfTarget = false, overrideStatus = false) {
constructor(effect: StatusEffect, selfTarget = false) {
super(selfTarget);
this.effect = effect;
@ -2455,18 +2454,11 @@ export class StatusEffectAttr extends MoveEffectAttr {
return false;
}
// non-status moves don't play sound effects for failures
const quiet = move.category !== MoveCategory.STATUS;
// TODO: why
const pokemon = this.selfTarget ? user : target;
if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, this.overrideStatus, user, true)) {
return false;
}
// TODO: What does a chance of -1 have to do with any of this???
if (
(!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0))
&& pokemon.trySetStatus(this.effect, true, user, null, this.overrideStatus, quiet)
this.doSetStatus(this.selfTarget ? user : target, user, quiet)
) {
applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
return true;
@ -2476,10 +2468,23 @@ export class StatusEffectAttr extends MoveEffectAttr {
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false);
const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1);
const score = moveChance < 0 ? -10 : Math.floor(moveChance * -0.1);
const pokemon = this.selfTarget ? user : target;
return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
return pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
}
/**
* Wrapper function to attempt to set status of a pokemon.
* Exists to allow super classes to override parameters.
* @param pokemon - The {@linkcode Pokemon} being statused.
* @param user - The {@linkcode Pokemon} doing the statusing.
* @param quiet - Whether to suppress messages for status immunities.
* @returns Whether the status was sucessfully applied.
* @see {@linkcode Pokemon.trySetStatus}
*/
protected doSetStatus(pokemon: Pokemon, user: Pokemon, quiet: boolean): boolean {
return pokemon.trySetStatus(this.effect, true, user, undefined, null, false, quiet)
}
}
@ -2490,21 +2495,15 @@ export class StatusEffectAttr extends MoveEffectAttr {
export class RestAttr extends StatusEffectAttr {
private duration: number;
constructor(
duration: number,
overrideStatus: boolean
){
constructor(duration: number) {
// Sleep is the only duration-based status ATM
super(StatusEffect.SLEEP, true, overrideStatus);
super(StatusEffect.SLEEP, true);
this.duration = duration;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const didStatus = super.apply(user, target, move, args);
if (didStatus && user.status?.effect === this.effect) {
user.status.sleepTurnsRemaining = this.duration;
}
return didStatus;
// TODO: Add custom text for rest and make `HealAttr` no longer cause status
protected override doSetStatus(pokemon: Pokemon, user: Pokemon, quiet: boolean): boolean {
return pokemon.trySetStatus(this.effect, true, user, this.duration, null, true, quiet)
}
}
@ -2543,25 +2542,39 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
* @returns `true` if Psycho Shift's effect is able to be applied to the target
*/
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined);
if (target.status) {
// Bang is justified as condition func returns early if no status is found
const statusToApply = user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : user.status?.effect!
if (!target.trySetStatus(statusToApply, true, user)) {
return false;
} else {
const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false;
}
if (trySetStatus && user.status) {
// PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move
user.addTag(BattlerTagType.PSYCHO_SHIFT);
if (user.status) {
// Add tag to user to heal its status effect after the move ends (unless we have comatose);
// Occurs after move use to ensure correct Synchronize timing
user.addTag(BattlerTagType.PSYCHO_SHIFT)
}
return true;
}
getCondition(): MoveConditionFunc {
return (user, target, _move) => {
if (target.status) {
return false;
}
return trySetStatus;
const statusToApply = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined);
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
}
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return !target.status && target.canSetStatus(user.status?.effect, true, false, user) ? -10 : 0;
const statusToApply =
user.status?.effect ??
(user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined);
// TODO: Give this an actual positive benefit score
return !target.status && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
}
}
@ -2831,7 +2844,10 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
*/
constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
super(selfTarget, { lastHitOnly: true });
this.effects = [ effects ].flat(1);
if (!Array.isArray(effects)) {
effects = [ effects ]
}
this.effects = effects
}
/**
@ -8747,8 +8763,8 @@ export function initMoves() {
.attr(MultiHitAttr, MultiHitType._2)
.makesContact(false),
new SelfStatusMove(Moves.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
.attr(RestAttr, 3, true)
.attr(HealAttr, 1, true)
.attr(RestAttr, 3)
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user))
.triageMove(),
new AttackMove(Moves.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
@ -9469,15 +9485,7 @@ export function initMoves() {
.makesContact(false)
.unimplemented(),
new StatusMove(Moves.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
.attr(PsychoShiftEffectAttr)
.condition((user, target, move) => {
let statusToApply = user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined;
if (user.status?.effect && isNonVolatileStatusEffect(user.status.effect)) {
statusToApply = user.status.effect;
}
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
}
),
.attr(PsychoShiftEffectAttr),
new AttackMove(Moves.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
.makesContact()
.attr(LessPPMorePowerAttr),

View File

@ -154,6 +154,7 @@ export function getRandomStatus(statusA: Status | null, statusB: Status | null):
return randIntRange(0, 2) ? statusA : statusB;
}
// TODO: Make this a type and remove these
/**
* Gets all non volatile status effects
* @returns A list containing all non volatile status effects

View File

@ -1,3 +1,5 @@
/** Enum representing all non-volatile status effects. */
// TODO: Add a type that excludes `NONE` and `FAINT`
export enum StatusEffect {
NONE,
POISON,

View File

@ -3719,7 +3719,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// Status moves remain unchanged on weight, this encourages 1-2
movePool = baseWeights
.filter(m => !this.moveset.some(
mo =>
mo =>
m[0] === mo.moveId ||
(allMoves[m[0]].hasAttr(SacrificialAttr) && mo.getMove().hasAttr(SacrificialAttr)) // Only one self-KO move allowed
))
@ -3754,7 +3754,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} else {
// Non-trainer pokemon just use normal weights
movePool = baseWeights.filter(m => !this.moveset.some(
mo =>
mo =>
m[0] === mo.moveId ||
(allMoves[m[0]].hasAttr(SacrificialAttr) && mo.getMove().hasAttr(SacrificialAttr)) // Only one self-KO move allowed
));
@ -5405,7 +5405,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
);
}
queueImmuneMessage(quiet: boolean, effect?: StatusEffect): void {
// TODO: Add messages for misty/electric terrain
private queueImmuneMessage(quiet: boolean, effect?: StatusEffect): void {
if (!effect || quiet) {
return;
}
@ -5426,14 +5427,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
*/
// TODO: Review and verify the message order precedence in mainline if multiple status-blocking effects are present at once
canSetStatus(
effect: StatusEffect | undefined,
effect: StatusEffect,
quiet = false,
overrideStatus = false,
sourcePokemon: Pokemon | null = null,
ignoreField = false,
): boolean {
if (effect !== StatusEffect.FAINT) {
// Status-overriding moves (ie Rest) fail if their respective status already exists
if (overrideStatus ? this.status?.effect === effect : this.status) {
this.queueImmuneMessage(quiet, effect);
return false;
@ -5450,19 +5453,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const types = this.getTypes(true, true);
// Check for specific immunities for certain statuses
let isImmune = false;
switch (effect) {
case StatusEffect.POISON:
case StatusEffect.TOXIC:
// Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity
const poisonImmunity = types.map(defType => {
// Check if the Pokemon is not immune to Poison/Toxic
// Check for type based immunities and/or Corrosion
isImmune = types.some(defType => {
if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) {
return false;
}
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
if (!sourcePokemon) {
return true;
}
const cancelImmunity = new BooleanHolder(false);
if (sourcePokemon) {
applyAbAttrs(
IgnoreTypeStatusEffectImmunityAbAttr,
sourcePokemon,
@ -5471,57 +5477,36 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
effect,
defType,
);
if (cancelImmunity.value) {
return false;
}
}
return true;
});
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
if (poisonImmunity.includes(true)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
}
return cancelImmunity.value;
});
break;
case StatusEffect.PARALYSIS:
if (this.isOfType(PokemonType.ELECTRIC)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
isImmune = this.isOfType(PokemonType.ELECTRIC)
break;
case StatusEffect.SLEEP:
if (
isImmune =
this.isGrounded() &&
globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC;
break;
case StatusEffect.FREEZE:
if (
isImmune =
this.isOfType(PokemonType.ICE) ||
(!ignoreField &&
globalScene?.arena?.weather?.weatherType &&
!ignoreField &&
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(
globalScene.arena.weather.weatherType,
))
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
globalScene.arena.weather?.weatherType ?? WeatherType.NONE,
)
break;
case StatusEffect.BURN:
if (this.isOfType(PokemonType.FIRE)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
isImmune = this.isOfType(PokemonType.FIRE)
break;
}
if (isImmune) {
this.queueImmuneMessage(quiet, effect)
return false;
}
// Check for cancellations from self/ally abilities
const cancelled = new BooleanHolder(false);
applyPreSetStatusAbAttrs(
StatusEffectImmunityAbAttr,
@ -5543,23 +5528,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
quiet, this, sourcePokemon,
)
if (cancelled.value) {
break;
return false;
}
}
if (cancelled.value) {
return false;
}
// Perform safeguard checks
if (
sourcePokemon &&
sourcePokemon !== this &&
this.isSafeguarded(sourcePokemon)
) {
if(!quiet){
if (!quiet) {
globalScene.queueMessage(
i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(this)
}));
i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(this)})
);
}
return false;
}
@ -5567,14 +5549,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return true;
}
// TODO: Make this take a destructured object as args to condense all these optional args...
trySetStatus(
effect?: StatusEffect,
effect: StatusEffect,
asPhase = false,
sourcePokemon: Pokemon | null = null,
turnsRemaining?: number,
sourceText: string | null = null,
overrideStatus?: boolean,
quiet = true,
): boolean {
// TODO: Remove uses of `asPhase=false` in favor of checking status directly
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
return false;
}
@ -5598,47 +5583,76 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (overrideStatus) {
this.resetStatus(false);
}
globalScene.unshiftPhase(
new ObtainStatusEffectPhase(
this.getBattlerIndex(),
effect,
turnsRemaining,
sourceText,
sourcePokemon,
),
);
return true;
} else {
this.doSetStatus(effect, turnsRemaining)
}
let sleepTurnsRemaining: NumberHolder;
return true;
}
/**
* Attempt to give the specified Pokemon the given status effect.
* Does **NOT** perform any feasibility checks whatsoever, and should thus never be called directly
* unless conditions are known to be met.
* @param effect - The {@linkcode StatusEffect} to set
*/
doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void;
/**
* Attempt to give the specified Pokemon the given status effect.
* Does **NOT** perform any feasibility checks whatsoever, and should thus never be called directly
* unless conditions are known to be met.
* @param effect - StatusEffect.SLEEP
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4.
*/
doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void;
/**
* Attempt to give the specified Pokemon the given status effect.
* Does **NOT** perform any feasibility checks whatsoever, and should thus never be called directly
* unless conditions are known to be met.
* @param effect - The {@linkcode StatusEffect} to set
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4.
*/
doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void;
/**
* Attempt to give the specified Pokemon the given status effect.
* Does **NOT** perform any feasibility checks whatsoever, and should thus never be called directly
* unless conditions are known to be met.
* @param effect - The {@linkcode StatusEffect} to set
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4.
*/
doSetStatus(effect: StatusEffect, sleepTurnsRemaining = this.randBattleSeedIntRange(2, 4)): void {
if (effect === StatusEffect.SLEEP) {
sleepTurnsRemaining = new NumberHolder(this.randBattleSeedIntRange(2, 4));
this.setFrameRate(4);
// If the user is invulnerable, lets remove their invulnerability when they fall asleep
const invulnerableTags = [
// If the user is invulnerable, remove their invulnerability when they fall asleep
const invulnTag = [
BattlerTagType.UNDERGROUND,
BattlerTagType.UNDERWATER,
BattlerTagType.HIDDEN,
BattlerTagType.FLYING,
];
].find(t => this.getTag(t));
const tag = invulnerableTags.find(t => this.getTag(t));
if (tag) {
this.removeTag(tag);
this.getMoveQueue().pop();
if (invulnTag) {
this.removeTag(invulnTag);
this.getMoveQueue().shift();
}
}
sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined
effect = effect!; // If `effect` is undefined then `trySetStatus()` will have already returned early via the `canSetStatus()` call
this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
return true;
this.status = new Status(effect, 0, sleepTurnsRemaining);
}
/**
* Resets the status of a pokemon.
* @param revive Whether revive should be cured; defaults to true.

View File

@ -1751,12 +1751,12 @@ export class TurnStatusEffectModifier extends PokemonHeldItemModifier {
}
/**
* Tries to inflicts the holder with the associated {@linkcode StatusEffect}.
* @param pokemon {@linkcode Pokemon} that holds the held item
* Attempt to inflicts the holder with the associated {@linkcode StatusEffect}.
* @param pokemon - The {@linkcode Pokemon} holds the item.
* @returns `true` if the status effect was applied successfully
*/
override apply(pokemon: Pokemon): boolean {
return pokemon.trySetStatus(this.effect, true, pokemon, this.type.name);
return pokemon.trySetStatus(this.effect, true, pokemon, undefined, this.type.name);
}
getMaxHeldItemCount(_pokemon: Pokemon): number {

View File

@ -12,19 +12,22 @@ import { isNullOrUndefined } from "#app/utils/common";
/** The phase where pokemon obtain status effects. */
export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect?: StatusEffect;
private statusEffect: StatusEffect;
private turnsRemaining?: number;
private sourceText?: string | null;
private sourcePokemon?: Pokemon | null;
constructor(
battlerIndex: BattlerIndex,
statusEffect?: StatusEffect,
statusEffect: StatusEffect,
turnsRemaining?: number,
sourceText?: string | null,
sourcePokemon?: Pokemon | null,
) {
super(battlerIndex);
this.statusEffect = statusEffect;
this.turnsRemaining = turnsRemaining;
this.sourceText = sourceText;
this.sourcePokemon = sourcePokemon;
}
@ -32,19 +35,19 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
start() {
const pokemon = this.getPokemon();
if (pokemon.status?.effect === this.statusEffect) {
globalScene.queueMessage(
getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)),
);
globalScene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon)));
this.end();
return;
}
if (!pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
// status application passes
if (!pokemon.canSetStatus(this.statusEffect, false, false, this.sourcePokemon)) {
// status application fails
this.end();
return;
}
pokemon.doSetStatus(this.statusEffect, this.turnsRemaining);
pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => {
globalScene.queueMessage(
@ -52,8 +55,6 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
);
if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true);
// If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards
// TODO: We may need to reset this for Ice Fang, etc.
globalScene.arena.setIgnoreAbilities(false);
applyPostSetStatusAbAttrs(PostSetStatusAbAttr, pokemon, this.statusEffect, this.sourcePokemon);
}

View File

@ -71,7 +71,7 @@ describe("Abilities - Corrosion", () => {
expect(enemyPokemon!.status).toBeUndefined();
});
it("should affect the user's held a Toxic Orb", async () => {
it("should affect the user's held Toxic Orb", async () => {
game.override.startingHeldItems([{ name: "TOXIC_ORB", count: 1 }]);
await game.classicMode.startBattle([Species.SALAZZLE]);

View File

@ -25,15 +25,6 @@ describe("Spec - Pokemon", () => {
game = new GameManager(phaserGame);
});
it("should not crash when trying to set status of undefined", async () => {
await game.classicMode.runToSummon([Species.ABRA]);
const pkm = game.scene.getPlayerPokemon()!;
expect(pkm).toBeDefined();
expect(pkm.trySetStatus(undefined)).toBe(true);
});
describe("Add To Party", () => {
let scene: BattleScene;

View File

@ -3,6 +3,7 @@ import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
@ -25,7 +26,7 @@ describe("Moves - Rest", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.REST, Moves.SLEEP_TALK])
.moveset([Moves.REST, Moves.SWORDS_DANCE])
.ability(Abilities.BALL_FETCH)
.battleStyle("single")
.disableCrits()
@ -34,12 +35,12 @@ describe("Moves - Rest", () => {
.enemyMoveset(Moves.SPLASH);
});
it("should fully heal the user, cure its status and put it to sleep", async () => {
it("should fully heal the user, cure its prior status and put it to sleep", async () => {
game.override.statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([Species.SNORLAX]);
const snorlax = game.scene.getPlayerPokemon()!;
snorlax.hp = 1;
snorlax.trySetStatus(StatusEffect.POISON);
expect(snorlax.status?.effect).toBe(StatusEffect.POISON);
game.move.select(Moves.REST);
@ -49,7 +50,35 @@ describe("Moves - Rest", () => {
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
});
it("should preserve non-volatile conditions", async () => {
it("should always last 3 turns", async () => {
await game.classicMode.startBattle([Species.SNORLAX]);
const snorlax = game.scene.getPlayerPokemon()!;
snorlax.hp = 1;
// Cf https://bulbapedia.bulbagarden.net/wiki/Rest_(move):
// > The user is unable to use moves while asleep for 2 turns after the turn when Rest is used.
game.move.select(Moves.REST);
await game.toNextTurn();
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
expect(snorlax.status?.sleepTurnsRemaining).toBe(3);
game.move.select(Moves.SWORDS_DANCE);
await game.toNextTurn();
expect(snorlax.status?.sleepTurnsRemaining).toBe(2);
game.move.select(Moves.SWORDS_DANCE);
await game.toNextTurn();
expect(snorlax.status?.sleepTurnsRemaining).toBe(1);
game.move.select(Moves.SWORDS_DANCE);
await game.toNextTurn();
expect(snorlax.status?.effect).toBeUndefined();
expect(snorlax.getStatStage(Stat.ATK)).toBe(2);
});
it("should preserve non-volatile status conditions", async () => {
await game.classicMode.startBattle([Species.SNORLAX]);
const snorlax = game.scene.getPlayerPokemon()!;
@ -64,15 +93,14 @@ describe("Moves - Rest", () => {
it.each<{ name: string; status?: StatusEffect; ability?: Abilities; dmg?: number }>([
{ name: "is at full HP", dmg: 0 },
{ name: "is affected by Electric Terrain", ability: Abilities.ELECTRIC_SURGE },
{ name: "is affected by Misty Terrain", ability: Abilities.MISTY_SURGE },
{ name: "is grounded on Electric Terrain", ability: Abilities.ELECTRIC_SURGE },
{ name: "is grounded on Misty Terrain", ability: Abilities.MISTY_SURGE },
{ name: "has Comatose", ability: Abilities.COMATOSE },
])("should fail if the user $name", async ({ status = StatusEffect.NONE, ability = Abilities.NONE, dmg = 1 }) => {
game.override.ability(ability);
game.override.ability(ability).statusEffect(status);
await game.classicMode.startBattle([Species.SNORLAX]);
const snorlax = game.scene.getPlayerPokemon()!;
snorlax.trySetStatus(status);
snorlax.hp = snorlax.getMaxHp() - dmg;
@ -83,21 +111,22 @@ describe("Moves - Rest", () => {
});
it("should fail if called while already asleep", async () => {
game.override.statusEffect(StatusEffect.SLEEP).moveset([Moves.REST, Moves.SLEEP_TALK]);
await game.classicMode.startBattle([Species.SNORLAX]);
const snorlax = game.scene.getPlayerPokemon()!;
snorlax.hp = 1;
snorlax.trySetStatus(StatusEffect.SLEEP);
game.move.select(Moves.SLEEP_TALK);
await game.phaseInterceptor.to("BerryPhase");
expect(snorlax.isFullHp()).toBe(false);
expect(snorlax.status?.effect).toBeUndefined();
expect(snorlax.getLastXMoves().map(tm => tm.result)).toEqual([MoveResult.FAIL, MoveResult.SUCCESS]);
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
expect(snorlax.getLastXMoves(-1).map(tm => tm.result)).toEqual([MoveResult.FAIL, MoveResult.SUCCESS]);
});
it("should succeed if called the turn after waking up", async () => {
game.override.statusEffect(StatusEffect.SLEEP);
await game.classicMode.startBattle([Species.SNORLAX]);
const snorlax = game.scene.getPlayerPokemon()!;
@ -112,6 +141,6 @@ describe("Moves - Rest", () => {
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
expect(snorlax.isFullHp()).toBe(true);
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(snorlax.status?.sleepTurnsRemaining).toBe(3);
expect(snorlax.status?.sleepTurnsRemaining).toBeGreaterThan(1);
});
});