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, this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC,
true, true,
null, null,
0,
this.getMoveName(), this.getMoveName(),
); );
} }

View File

@ -1873,11 +1873,11 @@ export class HealAttr extends MoveEffectAttr {
/** Should an animation be shown? */ /** Should an animation be shown? */
private showAnim: boolean; private showAnim: boolean;
constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) { constructor(healRatio = 1, showAnim = false, selfTarget = true) {
super(selfTarget === undefined || selfTarget); super(selfTarget);
this.healRatio = healRatio || 1; this.healRatio = healRatio;
this.showAnim = !!showAnim; this.showAnim = showAnim;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -2440,9 +2440,8 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
export class StatusEffectAttr extends MoveEffectAttr { export class StatusEffectAttr extends MoveEffectAttr {
public effect: StatusEffect; public effect: StatusEffect;
private overrideStatus: boolean;
constructor(effect: StatusEffect, selfTarget = false, overrideStatus = false) { constructor(effect: StatusEffect, selfTarget = false) {
super(selfTarget); super(selfTarget);
this.effect = effect; this.effect = effect;
@ -2455,18 +2454,11 @@ export class StatusEffectAttr extends MoveEffectAttr {
return false; return false;
} }
// non-status moves don't play sound effects for failures
const quiet = move.category !== MoveCategory.STATUS; 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 ( if (
(!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) this.doSetStatus(this.selfTarget ? user : target, user, quiet)
&& pokemon.trySetStatus(this.effect, true, user, null, this.overrideStatus, quiet)
) { ) {
applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
return true; return true;
@ -2476,10 +2468,23 @@ export class StatusEffectAttr extends MoveEffectAttr {
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); 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; 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 { export class RestAttr extends StatusEffectAttr {
private duration: number; private duration: number;
constructor( constructor(duration: number) {
duration: number,
overrideStatus: boolean
){
// Sleep is the only duration-based status ATM // Sleep is the only duration-based status ATM
super(StatusEffect.SLEEP, true, overrideStatus); super(StatusEffect.SLEEP, true);
this.duration = duration; this.duration = duration;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { // TODO: Add custom text for rest and make `HealAttr` no longer cause status
const didStatus = super.apply(user, target, move, args); protected override doSetStatus(pokemon: Pokemon, user: Pokemon, quiet: boolean): boolean {
if (didStatus && user.status?.effect === this.effect) { return pokemon.trySetStatus(this.effect, true, user, this.duration, null, true, quiet)
user.status.sleepTurnsRemaining = this.duration;
}
return didStatus;
} }
} }
@ -2543,25 +2542,39 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
* @returns `true` if Psycho Shift's effect is able to be applied to the target * @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 { apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined); // 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.status) { if (!target.trySetStatus(statusToApply, true, user)) {
return false; 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);
} }
return trySetStatus; 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;
}
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 { 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[]) { constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
super(selfTarget, { lastHitOnly: true }); 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) .attr(MultiHitAttr, MultiHitType._2)
.makesContact(false), .makesContact(false),
new SelfStatusMove(Moves.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1) new SelfStatusMove(Moves.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
.attr(RestAttr, 3, true)
.attr(HealAttr, 1, true) .attr(HealAttr, 1, true)
.attr(RestAttr, 3)
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user)) .condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user))
.triageMove(), .triageMove(),
new AttackMove(Moves.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1) new AttackMove(Moves.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
@ -9469,15 +9485,7 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.unimplemented(), .unimplemented(),
new StatusMove(Moves.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4) new StatusMove(Moves.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
.attr(PsychoShiftEffectAttr) .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);
}
),
new AttackMove(Moves.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4) new AttackMove(Moves.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
.makesContact() .makesContact()
.attr(LessPPMorePowerAttr), .attr(LessPPMorePowerAttr),

View File

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

View File

@ -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) { if (!effect || quiet) {
return; 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 sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered * @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( canSetStatus(
effect: StatusEffect | undefined, effect: StatusEffect,
quiet = false, quiet = false,
overrideStatus = false, overrideStatus = false,
sourcePokemon: Pokemon | null = null, sourcePokemon: Pokemon | null = null,
ignoreField = false, ignoreField = false,
): boolean { ): boolean {
if (effect !== StatusEffect.FAINT) { if (effect !== StatusEffect.FAINT) {
// Status-overriding moves (ie Rest) fail if their respective status already exists
if (overrideStatus ? this.status?.effect === effect : this.status) { if (overrideStatus ? this.status?.effect === effect : this.status) {
this.queueImmuneMessage(quiet, effect); this.queueImmuneMessage(quiet, effect);
return false; return false;
@ -5450,19 +5453,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const types = this.getTypes(true, true); const types = this.getTypes(true, true);
// Check for specific immunities for certain statuses
let isImmune = false;
switch (effect) { switch (effect) {
case StatusEffect.POISON: case StatusEffect.POISON:
case StatusEffect.TOXIC: case StatusEffect.TOXIC:
// Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity // Check for type based immunities and/or Corrosion
const poisonImmunity = types.map(defType => { isImmune = types.some(defType => {
// Check if the Pokemon is not immune to Poison/Toxic
if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) { if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) {
return false; 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); const cancelImmunity = new BooleanHolder(false);
if (sourcePokemon) {
applyAbAttrs( applyAbAttrs(
IgnoreTypeStatusEffectImmunityAbAttr, IgnoreTypeStatusEffectImmunityAbAttr,
sourcePokemon, sourcePokemon,
@ -5471,57 +5477,36 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
effect, effect,
defType, defType,
); );
if (cancelImmunity.value) { return 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;
}
}
break; break;
case StatusEffect.PARALYSIS: case StatusEffect.PARALYSIS:
if (this.isOfType(PokemonType.ELECTRIC)) { isImmune = this.isOfType(PokemonType.ELECTRIC)
this.queueImmuneMessage(quiet, effect);
return false;
}
break; break;
case StatusEffect.SLEEP: case StatusEffect.SLEEP:
if ( isImmune =
this.isGrounded() && this.isGrounded() &&
globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC;
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break; break;
case StatusEffect.FREEZE: case StatusEffect.FREEZE:
if ( isImmune =
this.isOfType(PokemonType.ICE) || this.isOfType(PokemonType.ICE) ||
(!ignoreField && !ignoreField &&
globalScene?.arena?.weather?.weatherType &&
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes( [WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(
globalScene.arena.weather.weatherType, globalScene.arena.weather?.weatherType ?? WeatherType.NONE,
)) )
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break; break;
case StatusEffect.BURN: case StatusEffect.BURN:
if (this.isOfType(PokemonType.FIRE)) { isImmune = this.isOfType(PokemonType.FIRE)
this.queueImmuneMessage(quiet, effect);
return false;
}
break; break;
} }
if (isImmune) {
this.queueImmuneMessage(quiet, effect)
return false;
}
// Check for cancellations from self/ally abilities
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyPreSetStatusAbAttrs( applyPreSetStatusAbAttrs(
StatusEffectImmunityAbAttr, StatusEffectImmunityAbAttr,
@ -5542,24 +5527,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
cancelled, cancelled,
quiet, this, sourcePokemon, quiet, this, sourcePokemon,
) )
if (cancelled.value) {
break;
}
}
if (cancelled.value) { if (cancelled.value) {
return false; return false;
} }
}
// Perform safeguard checks
if ( if (
sourcePokemon && sourcePokemon &&
sourcePokemon !== this && sourcePokemon !== this &&
this.isSafeguarded(sourcePokemon) this.isSafeguarded(sourcePokemon)
) { ) {
if(!quiet){ if (!quiet) {
globalScene.queueMessage( globalScene.queueMessage(
i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(this) i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(this)})
})); );
} }
return false; return false;
} }
@ -5567,14 +5549,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return true; return true;
} }
// TODO: Make this take a destructured object as args to condense all these optional args...
trySetStatus( trySetStatus(
effect?: StatusEffect, effect: StatusEffect,
asPhase = false, asPhase = false,
sourcePokemon: Pokemon | null = null, sourcePokemon: Pokemon | null = null,
turnsRemaining?: number,
sourceText: string | null = null, sourceText: string | null = null,
overrideStatus?: boolean, overrideStatus?: boolean,
quiet = true, quiet = true,
): boolean { ): boolean {
// TODO: Remove uses of `asPhase=false` in favor of checking status directly
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) { if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
return false; return false;
} }
@ -5598,47 +5583,76 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (overrideStatus) { if (overrideStatus) {
this.resetStatus(false); this.resetStatus(false);
} }
globalScene.unshiftPhase( globalScene.unshiftPhase(
new ObtainStatusEffectPhase( new ObtainStatusEffectPhase(
this.getBattlerIndex(), this.getBattlerIndex(),
effect, effect,
turnsRemaining,
sourceText, sourceText,
sourcePokemon, sourcePokemon,
), ),
); );
} else {
this.doSetStatus(effect, turnsRemaining)
}
return true; return true;
} }
let sleepTurnsRemaining: NumberHolder; /**
* 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) { if (effect === StatusEffect.SLEEP) {
sleepTurnsRemaining = new NumberHolder(this.randBattleSeedIntRange(2, 4));
this.setFrameRate(4); this.setFrameRate(4);
// If the user is invulnerable, lets remove their invulnerability when they fall asleep // If the user is invulnerable, remove their invulnerability when they fall asleep
const invulnerableTags = [ const invulnTag = [
BattlerTagType.UNDERGROUND, BattlerTagType.UNDERGROUND,
BattlerTagType.UNDERWATER, BattlerTagType.UNDERWATER,
BattlerTagType.HIDDEN, BattlerTagType.HIDDEN,
BattlerTagType.FLYING, BattlerTagType.FLYING,
]; ].find(t => this.getTag(t));
const tag = invulnerableTags.find(t => this.getTag(t)); if (invulnTag) {
this.removeTag(invulnTag);
if (tag) { this.getMoveQueue().shift();
this.removeTag(tag);
this.getMoveQueue().pop();
} }
} }
sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined this.status = new Status(effect, 0, sleepTurnsRemaining);
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;
} }
/** /**
* Resets the status of a pokemon. * Resets the status of a pokemon.
* @param revive Whether revive should be cured; defaults to true. * @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}. * Attempt to inflicts the holder with the associated {@linkcode StatusEffect}.
* @param pokemon {@linkcode Pokemon} that holds the held item * @param pokemon - The {@linkcode Pokemon} holds the item.
* @returns `true` if the status effect was applied successfully * @returns `true` if the status effect was applied successfully
*/ */
override apply(pokemon: Pokemon): boolean { 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 { getMaxHeldItemCount(_pokemon: Pokemon): number {

View File

@ -12,19 +12,22 @@ import { isNullOrUndefined } from "#app/utils/common";
/** The phase where pokemon obtain status effects. */ /** The phase where pokemon obtain status effects. */
export class ObtainStatusEffectPhase extends PokemonPhase { export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect?: StatusEffect; private statusEffect: StatusEffect;
private turnsRemaining?: number;
private sourceText?: string | null; private sourceText?: string | null;
private sourcePokemon?: Pokemon | null; private sourcePokemon?: Pokemon | null;
constructor( constructor(
battlerIndex: BattlerIndex, battlerIndex: BattlerIndex,
statusEffect?: StatusEffect, statusEffect: StatusEffect,
turnsRemaining?: number,
sourceText?: string | null, sourceText?: string | null,
sourcePokemon?: Pokemon | null, sourcePokemon?: Pokemon | null,
) { ) {
super(battlerIndex); super(battlerIndex);
this.statusEffect = statusEffect; this.statusEffect = statusEffect;
this.turnsRemaining = turnsRemaining;
this.sourceText = sourceText; this.sourceText = sourceText;
this.sourcePokemon = sourcePokemon; this.sourcePokemon = sourcePokemon;
} }
@ -32,19 +35,19 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
start() { start() {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (pokemon.status?.effect === this.statusEffect) { if (pokemon.status?.effect === this.statusEffect) {
globalScene.queueMessage( globalScene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon)));
getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)),
);
this.end(); this.end();
return; return;
} }
if (!pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { if (!pokemon.canSetStatus(this.statusEffect, false, false, this.sourcePokemon)) {
// status application passes // status application fails
this.end(); this.end();
return; return;
} }
pokemon.doSetStatus(this.statusEffect, this.turnsRemaining);
pokemon.updateInfo(true); pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => { new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => {
globalScene.queueMessage( globalScene.queueMessage(
@ -52,8 +55,6 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
); );
if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) { if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true); 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); globalScene.arena.setIgnoreAbilities(false);
applyPostSetStatusAbAttrs(PostSetStatusAbAttr, pokemon, this.statusEffect, this.sourcePokemon); applyPostSetStatusAbAttrs(PostSetStatusAbAttr, pokemon, this.statusEffect, this.sourcePokemon);
} }

View File

@ -71,7 +71,7 @@ describe("Abilities - Corrosion", () => {
expect(enemyPokemon!.status).toBeUndefined(); 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 }]); game.override.startingHeldItems([{ name: "TOXIC_ORB", count: 1 }]);
await game.classicMode.startBattle([Species.SALAZZLE]); await game.classicMode.startBattle([Species.SALAZZLE]);

View File

@ -25,15 +25,6 @@ describe("Spec - Pokemon", () => {
game = new GameManager(phaserGame); 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", () => { describe("Add To Party", () => {
let scene: BattleScene; let scene: BattleScene;

View File

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