diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 0f09da6b2bc..89ac2552318 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -856,6 +856,7 @@ class ToxicSpikesTag extends ArenaTrapTag { this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, + 0, this.getMoveName(), ); } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index cd1d833a9bf..bec6fde98a5 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -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), diff --git a/src/data/status-effect.ts b/src/data/status-effect.ts index a90304c9f7d..5fa3d99568c 100644 --- a/src/data/status-effect.ts +++ b/src/data/status-effect.ts @@ -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 diff --git a/src/enums/status-effect.ts b/src/enums/status-effect.ts index b79951f530a..aa5326f4cde 100644 --- a/src/enums/status-effect.ts +++ b/src/enums/status-effect.ts @@ -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, diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6168e4e3658..ec5a5712c46 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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): 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. diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index e6a34ed03fc..e5acb8bde72 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -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 { diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index 452b48f9109..3e27eb001e7 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -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); } diff --git a/test/abilities/corrosion.test.ts b/test/abilities/corrosion.test.ts index d054cbd5a39..cbbc5ee339b 100644 --- a/test/abilities/corrosion.test.ts +++ b/test/abilities/corrosion.test.ts @@ -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]); diff --git a/test/field/pokemon.test.ts b/test/field/pokemon.test.ts index f763ab2c401..6472d4a1245 100644 --- a/test/field/pokemon.test.ts +++ b/test/field/pokemon.test.ts @@ -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; diff --git a/test/moves/rest.test.ts b/test/moves/rest.test.ts index d7a4e06497f..eff1688251e 100644 --- a/test/moves/rest.test.ts +++ b/test/moves/rest.test.ts @@ -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); }); });