mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-20 16:42:45 +02:00
Merge fefa8e408f
into 4b70fab608
This commit is contained in:
commit
20c19a8015
@ -1544,7 +1544,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
|
||||
): void {
|
||||
const effect =
|
||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||
attacker.trySetStatus(effect, true, pokemon);
|
||||
attacker.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2879,7 +2879,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
|
||||
): void {
|
||||
const effect =
|
||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||
attacker.trySetStatus(effect, true, pokemon);
|
||||
attacker.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3086,7 +3086,7 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr {
|
||||
_args: any[],
|
||||
): void {
|
||||
if (!simulated && sourcePokemon) {
|
||||
sourcePokemon.trySetStatus(effect, true, pokemon);
|
||||
sourcePokemon.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4452,7 +4452,7 @@ export class PreSetStatusAbAttr extends AbAttr {
|
||||
_pokemon: Pokemon,
|
||||
_passive: boolean,
|
||||
_simulated: boolean,
|
||||
_effect: StatusEffect | undefined,
|
||||
_effect: StatusEffect,
|
||||
_cancelled: BooleanHolder,
|
||||
_args: any[],
|
||||
): boolean {
|
||||
@ -4463,7 +4463,7 @@ export class PreSetStatusAbAttr extends AbAttr {
|
||||
_pokemon: Pokemon,
|
||||
_passive: boolean,
|
||||
_simulated: boolean,
|
||||
_effect: StatusEffect | undefined,
|
||||
_effect: StatusEffect,
|
||||
_cancelled: BooleanHolder,
|
||||
_args: any[],
|
||||
): void {}
|
||||
@ -8646,8 +8646,7 @@ export function initAbilities() {
|
||||
.unsuppressable()
|
||||
.bypassFaint(),
|
||||
new Ability(AbilityId.CORROSION, 7)
|
||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ])
|
||||
.edgeCase(), // Should poison itself with toxic orb.
|
||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]),
|
||||
new Ability(AbilityId.COMATOSE, 7)
|
||||
.attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects())
|
||||
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||
|
@ -583,7 +583,7 @@ export function applyPostStatStageChangeAbAttrs<K extends AbAttrString>(
|
||||
export function applyPreSetStatusAbAttrs<K extends AbAttrString>(
|
||||
attrType: AbAttrMap[K] extends PreSetStatusAbAttr ? K : never,
|
||||
pokemon: Pokemon,
|
||||
effect: StatusEffect | undefined,
|
||||
effect: StatusEffect,
|
||||
cancelled: BooleanHolder,
|
||||
simulated = false,
|
||||
...args: any[]
|
||||
|
@ -553,7 +553,7 @@ class WishTag extends ArenaTag {
|
||||
const target = globalScene.getField()[this.battlerIndex];
|
||||
if (target?.isActive(true)) {
|
||||
globalScene.phaseManager.queueMessage(this.triggerMessage);
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), this.healHp, null, true, false);
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), this.healHp, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -812,32 +812,38 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||
this.neutralized = true;
|
||||
if (globalScene.arena.removeTag(this.tagType)) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} else if (!pokemon.status) {
|
||||
const toxic = this.layers > 1;
|
||||
if (
|
||||
pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, this.getMoveName())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!pokemon.isGrounded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// poison types will neutralize toxic spikes
|
||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||
this.neutralized = true;
|
||||
if (globalScene.arena.removeTag(this.tagType)) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pokemon.status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pokemon.trySetStatus(
|
||||
this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC,
|
||||
null,
|
||||
0,
|
||||
this.getMoveName(),
|
||||
);
|
||||
}
|
||||
|
||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||
|
@ -467,7 +467,7 @@ export class BeakBlastChargingTag extends BattlerTag {
|
||||
target: pokemon,
|
||||
})
|
||||
) {
|
||||
phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
|
||||
phaseData.attacker.trySetStatus(StatusEffect.BURN, pokemon);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -1355,7 +1355,7 @@ export class DrowsyTag extends BattlerTag {
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (!super.lapse(pokemon, lapseType)) {
|
||||
pokemon.trySetStatus(StatusEffect.SLEEP, true);
|
||||
pokemon.trySetStatus(StatusEffect.SLEEP);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1674,7 +1674,7 @@ export class ContactSetStatusProtectedTag extends DamageProtectedTag {
|
||||
* @param user - The pokemon that is being attacked and has the tag
|
||||
*/
|
||||
override onContact(attacker: Pokemon, user: Pokemon): void {
|
||||
attacker.trySetStatus(this.statusEffect, true, user);
|
||||
attacker.trySetStatus(this.statusEffect, user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2641,7 +2641,7 @@ export class GulpMissileTag extends BattlerTag {
|
||||
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
||||
globalScene.phaseManager.unshiftNew("StatStageChangePhase", attacker.getBattlerIndex(), false, [Stat.DEF], -1);
|
||||
} else {
|
||||
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
|
||||
attacker.trySetStatus(StatusEffect.PARALYSIS, pokemon);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
} from "../status-effect";
|
||||
import { getTypeDamageMultiplier } from "../type";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { BooleanHolder, NumberHolder, isNullOrUndefined, toDmgValue, randSeedItem, randSeedInt, getEnumValues, toReadableString, type Constructor, randSeedFloat } from "#app/utils/common";
|
||||
import { BooleanHolder, NumberHolder, isNullOrUndefined, toDmgValue, randSeedItem, randSeedInt, getEnumValues, toReadableString, type Constructor, randSeedFloat, coerceArray } from "#app/utils/common";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import type { ArenaTrapTag } from "../arena-tag";
|
||||
import { WeakenMoveTypeTag } from "../arena-tag";
|
||||
@ -1165,8 +1165,9 @@ export abstract class MoveAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* @virtual
|
||||
* @returns the {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} for this {@linkcode Move}
|
||||
* Return this `MoveAttr`'s associated {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}.
|
||||
* The specified condition will be added to all {@linkcode Move}s with this attribute,
|
||||
* and moves **will fail upon use** if _at least 1_ of their attached conditions returns `false`.
|
||||
*/
|
||||
getCondition(): MoveCondition | MoveConditionFunc | null {
|
||||
return null;
|
||||
@ -1279,15 +1280,21 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the {@linkcode Move}'s effects are valid to {@linkcode apply}
|
||||
* @virtual
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param args Set of unique arguments needed by this attribute
|
||||
* @returns true if basic application of the ability attribute should be possible
|
||||
* Determine whether this {@linkcode MoveAttr}'s effects are able to {@linkcode apply | be applied} to the target.
|
||||
*
|
||||
* Will **NOT** cause the move to fail upon returning `false` (unlike {@linkcode getCondition};
|
||||
* merely that the effect for this attribute will be nullified.
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} being targeted by the move, or {@linkcode user} if the move is
|
||||
* {@linkcode selfTarget | self-targeting}
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param _args - Set of unique arguments needed by this attribute
|
||||
* @returns `true` if basic application of this `MoveAttr`s effects should be possible
|
||||
*/
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]) {
|
||||
// TODO: Decouple this check from the `apply` step
|
||||
// TODO: Make non-damaging moves fail by default if none of their attributes can apply
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]) {
|
||||
// TODO: These checks seem redundant
|
||||
return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp)
|
||||
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) ||
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }));
|
||||
@ -1925,19 +1932,17 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class HealAttr extends MoveEffectAttr {
|
||||
/** The percentage of {@linkcode Stat.HP} to heal */
|
||||
private healRatio: number;
|
||||
/** Should an animation be shown? */
|
||||
private showAnim: boolean;
|
||||
|
||||
constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) {
|
||||
super(selfTarget === undefined || selfTarget);
|
||||
|
||||
this.healRatio = healRatio || 1;
|
||||
this.showAnim = !!showAnim;
|
||||
constructor(
|
||||
/** The percentage of {@linkcode Stat.HP} to heal. */
|
||||
private healRatio: number,
|
||||
/** Whether to display a healing animation when healing the target; default `false` */
|
||||
private showAnim = false,
|
||||
selfTarget = true
|
||||
) {
|
||||
super(selfTarget);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
this.addHealPhase(this.selfTarget ? user : target, this.healRatio);
|
||||
return true;
|
||||
}
|
||||
@ -1946,15 +1951,64 @@ export class HealAttr extends MoveEffectAttr {
|
||||
* Creates a new {@linkcode PokemonHealPhase}.
|
||||
* This heals the target and shows the appropriate message.
|
||||
*/
|
||||
addHealPhase(target: Pokemon, healRatio: number) {
|
||||
protected addHealPhase(target: Pokemon, healRatio: number) {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(),
|
||||
toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim);
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
override getTargetBenefitScore(user: Pokemon, target: Pokemon, _move: Move): number {
|
||||
const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10;
|
||||
return Math.round(score / (1 - this.healRatio / 2));
|
||||
}
|
||||
|
||||
// TODO: Change post move failure rework
|
||||
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||
if (!super.canApply(user, target, _move, _args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const healedPokemon = this.selfTarget ? user : target;
|
||||
if (healedPokemon.isFullHp()) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(healedPokemon),
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to put the user to sleep for a fixed duration, fully heal them and cure their status.
|
||||
* Used for {@linkcode MoveId.REST}.
|
||||
*/
|
||||
export class RestAttr extends HealAttr {
|
||||
private duration: number;
|
||||
|
||||
constructor(duration: number) {
|
||||
super(1, true);
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const wasSet = user.trySetStatus(StatusEffect.SLEEP, user, this.duration, null, true, true,
|
||||
i18next.t("moveTriggers:restBecameHealthy", {
|
||||
pokemonName: getPokemonNameWithAffix(user),
|
||||
}));
|
||||
return wasSet && super.apply(user, target, move, args);
|
||||
}
|
||||
|
||||
override addHealPhase(user: Pokemon): void {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", user.getBattlerIndex(), user.getMaxHp(), null)
|
||||
}
|
||||
|
||||
override getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) =>
|
||||
super.canApply(user, target, move, [])
|
||||
// Intentionally suppress messages here as we display generic fail msg
|
||||
// TODO: This might have order-of-operation jank
|
||||
&& user.canSetStatus(StatusEffect.SLEEP, true, true, user)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2226,20 +2280,9 @@ export class BoostHealAttr extends HealAttr {
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class HealOnAllyAttr extends HealAttr {
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param args N/A
|
||||
* @returns true if the function succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (user.getAlly() === target) {
|
||||
super.apply(user, target, move, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||
// Don't trigger if not targeting an ally
|
||||
return target === user.getAlly() && super.canApply(user, target, _move, _args);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2250,6 +2293,7 @@ export class HealOnAllyAttr extends HealAttr {
|
||||
* @see {@linkcode apply}
|
||||
* @see {@linkcode getUserBenefitScore}
|
||||
*/
|
||||
// TODO: Make Strength Sap its own attribute that extends off of this one
|
||||
export class HitHealAttr extends MoveEffectAttr {
|
||||
private healRatio: number;
|
||||
private healStat: EffectiveStat | null;
|
||||
@ -2500,49 +2544,50 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
|
||||
|
||||
export class StatusEffectAttr extends MoveEffectAttr {
|
||||
public effect: StatusEffect;
|
||||
public turnsRemaining?: number;
|
||||
public overrideStatus: boolean = false;
|
||||
|
||||
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
|
||||
constructor(effect: StatusEffect, selfTarget = false) {
|
||||
super(selfTarget);
|
||||
|
||||
this.effect = effect;
|
||||
this.turnsRemaining = turnsRemaining;
|
||||
this.overrideStatus = overrideStatus;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance;
|
||||
if (!statusCheck) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// non-status moves don't play sound effects for failures
|
||||
const quiet = move.category !== MoveCategory.STATUS;
|
||||
if (statusCheck) {
|
||||
const pokemon = this.selfTarget ? user : target;
|
||||
if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, false, user, true)) {
|
||||
return false;
|
||||
}
|
||||
if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
|
||||
&& pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) {
|
||||
applyPostAttackAbAttrs("ConfusionOnStatusEffectAbAttr", user, target, move, null, false, this.effect);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
target.trySetStatus(this.effect, user, undefined, null, false, quiet)
|
||||
) {
|
||||
applyPostAttackAbAttrs("ConfusionOnStatusEffectAbAttr", user, target, move, null, false, this.effect);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to randomly apply one of several statuses to the target.
|
||||
* Used for {@linkcode Moves.TRI_ATTACK} and {@linkcode Moves.DIRE_CLAW}.
|
||||
*/
|
||||
export class MultiStatusEffectAttr extends StatusEffectAttr {
|
||||
public effects: StatusEffect[];
|
||||
|
||||
constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
|
||||
super(effects[0], selfTarget, turnsRemaining, overrideStatus);
|
||||
constructor(effects: StatusEffect[], selfTarget?: boolean) {
|
||||
super(effects[0], selfTarget);
|
||||
this.effects = effects;
|
||||
}
|
||||
|
||||
@ -2572,25 +2617,41 @@ 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(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||
const statusToApply = user.status?.effect ??
|
||||
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||
|
||||
if (target.status) {
|
||||
// Bang is justified as condition func returns early if no status is found
|
||||
if (!target.trySetStatus(statusToApply!, 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) => {
|
||||
if (target.status?.effect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return trySetStatus;
|
||||
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.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(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||
|
||||
// TODO: Give this a positive user benefit score
|
||||
return !target.status?.effect && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2650,7 +2711,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
||||
* Used for Incinerate and Knock Off.
|
||||
* Not Implemented Cases: (Same applies for Thief)
|
||||
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item."
|
||||
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item.""
|
||||
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item."
|
||||
*/
|
||||
export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||
|
||||
@ -2860,7 +2921,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
||||
*/
|
||||
constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
|
||||
super(selfTarget, { lastHitOnly: true });
|
||||
this.effects = [ effects ].flat(1);
|
||||
this.effects = coerceArray(effects)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4350,6 +4411,10 @@ export class SpitUpPowerAttr extends VariablePowerAttr {
|
||||
* Does NOT remove stockpiled stacks.
|
||||
*/
|
||||
export class SwallowHealAttr extends HealAttr {
|
||||
constructor() {
|
||||
super(1)
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const stockpilingTag = user.getTag(StockpilingTag);
|
||||
|
||||
@ -7848,7 +7913,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (target.turnData.statStagesIncreased) {
|
||||
target.trySetStatus(this.effect, true, user);
|
||||
target.trySetStatus(this.effect, user);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -7995,11 +8060,11 @@ const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
||||
return !cancelled.value;
|
||||
};
|
||||
|
||||
const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE);
|
||||
const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.phaseQueue.find(phase => phase.is("MovePhase")) !== undefined;
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined;
|
||||
|
||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
@ -8881,9 +8946,7 @@ export function initMoves() {
|
||||
.attr(MultiHitAttr, MultiHitType._2)
|
||||
.makesContact(false),
|
||||
new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true)
|
||||
.attr(HealAttr, 1, true)
|
||||
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user))
|
||||
.attr(RestAttr, 3)
|
||||
.triageMove(),
|
||||
new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
|
||||
.attr(FlinchAttr)
|
||||
@ -9218,14 +9281,16 @@ export function initMoves() {
|
||||
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
||||
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 10, -1, 0, 3)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(SpitUpPowerAttr, 100)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
||||
new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(SwallowHealAttr)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true)
|
||||
.triageMove(),
|
||||
.triageMove()
|
||||
// TODO: Verify if using Swallow at full HP still consumes stacks or not
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.HEAT_WAVE, PokemonType.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN)
|
||||
@ -9607,14 +9672,8 @@ export function initMoves() {
|
||||
.unimplemented(),
|
||||
new StatusMove(MoveId.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
|
||||
.attr(PsychoShiftEffectAttr)
|
||||
.condition((user, target, move) => {
|
||||
let statusToApply = user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined;
|
||||
if (user.status?.effect && isNonVolatileStatusEffect(user.status.effect)) {
|
||||
statusToApply = user.status.effect;
|
||||
}
|
||||
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
||||
}
|
||||
),
|
||||
// TODO: Verify status applied if a statused pokemon obtains Comatose (via Transform) and uses Psycho Shift
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
|
||||
.makesContact()
|
||||
.attr(LessPPMorePowerAttr),
|
||||
|
@ -243,8 +243,9 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
|
||||
if (burnable?.length > 0) {
|
||||
const roll = randSeedInt(burnable.length);
|
||||
const chosenPokemon = burnable[roll];
|
||||
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
|
||||
if (chosenPokemon.canSetStatus(StatusEffect.BURN, true)) {
|
||||
// Burn applied
|
||||
chosenPokemon.doSetStatus(StatusEffect.BURN);
|
||||
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
|
||||
encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name);
|
||||
queueEncounterMessage(`${namespace}:option.2.target_burned`);
|
||||
|
@ -304,7 +304,7 @@ export function getRandomSpeciesByStarterCost(
|
||||
*/
|
||||
export function koPlayerPokemon(pokemon: PlayerPokemon) {
|
||||
pokemon.hp = 0;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||
pokemon.updateInfo();
|
||||
queueEncounterMessage(
|
||||
i18next.t("battle:fainted", {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/** Enum representing all non-volatile status effects. */
|
||||
// TODO: Remove StatusEffect.FAINT
|
||||
export enum StatusEffect {
|
||||
NONE,
|
||||
POISON,
|
||||
|
@ -4588,22 +4588,29 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a status effect can be applied to the Pokemon.
|
||||
* Check if a status effect can be applied to this {@linkcode Pokemon}.
|
||||
*
|
||||
* @param effect The {@linkcode StatusEffect} whose applicability is being checked
|
||||
* @param quiet Whether in-battle messages should trigger or not
|
||||
* @param overrideStatus Whether the Pokemon's current status can be overriden
|
||||
* @param sourcePokemon The Pokemon that is setting the status effect
|
||||
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
|
||||
* @param effect - The {@linkcode StatusEffect} whose applicability is being checked.
|
||||
* @param quiet - Whether to suppress in-battle messages for status checks; default `false`.
|
||||
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`.
|
||||
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`.
|
||||
* @param ignoreField - Whether to ignore field effects (weather, terrain, etc.) preventing status application;
|
||||
* default `false`
|
||||
* @returns Whether {@linkcode effect} can be applied to this Pokemon.
|
||||
*/
|
||||
// TODO: Review and verify the message order precedence in mainline if multiple status-blocking effects are present at once
|
||||
// TODO: Make argument order consistent with `trySetStatus`
|
||||
canSetStatus(
|
||||
effect: StatusEffect | undefined,
|
||||
effect: StatusEffect,
|
||||
quiet = false,
|
||||
overrideStatus = false,
|
||||
sourcePokemon: Pokemon | null = null,
|
||||
ignoreField = false,
|
||||
): boolean {
|
||||
if (effect !== StatusEffect.FAINT) {
|
||||
// Status-overriding moves (i.e. Rest) fail if their respective status already exists;
|
||||
// all other moves fail if the target already has _any_ status
|
||||
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
return false;
|
||||
@ -4616,67 +4623,51 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
const types = this.getTypes(true, true);
|
||||
|
||||
/* Whether the target is immune to the specific status being applied. */
|
||||
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
|
||||
case StatusEffect.TOXIC:
|
||||
// Check for type based immunities and/or Corrosion from the applier
|
||||
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, cancelImmunity, false, effect, defType);
|
||||
if (cancelImmunity.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", sourcePokemon, cancelImmunity, false, effect, defType);
|
||||
return !cancelImmunity.value;
|
||||
});
|
||||
|
||||
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
|
||||
if (poisonImmunity.includes(true)) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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 (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
return false;
|
||||
}
|
||||
isImmune = this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC;
|
||||
break;
|
||||
case StatusEffect.FREEZE:
|
||||
if (
|
||||
case StatusEffect.FREEZE: {
|
||||
const weatherType = globalScene.arena.weather?.weatherType;
|
||||
isImmune =
|
||||
this.isOfType(PokemonType.ICE) ||
|
||||
(!ignoreField &&
|
||||
globalScene?.arena?.weather?.weatherType &&
|
||||
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType))
|
||||
) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
return false;
|
||||
}
|
||||
(!ignoreField && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN));
|
||||
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", this, effect, cancelled, quiet);
|
||||
if (cancelled.value) {
|
||||
@ -4694,14 +4685,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
sourcePokemon,
|
||||
);
|
||||
if (cancelled.value) {
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform safeguard checks
|
||||
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
|
||||
if (!quiet) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -4714,15 +4702,31 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to set this Pokemon's status to the specified condition.
|
||||
* Enqueues a new `ObtainStatusEffectPhase` to trigger animations, etc.
|
||||
* @param effect - The {@linkcode StatusEffect} to set.
|
||||
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`.
|
||||
* @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for;
|
||||
* defaults to a random number between 2 and 4 and is unused for non-Sleep statuses.
|
||||
* @param sourceText - The text to show for the source of the status effect, if any; default `null`.
|
||||
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`.
|
||||
* @param quiet - Whether to suppress in-battle messages for status checks; default `true`.
|
||||
* @param overrideMessage - A string containing text to be displayed upon status setting; defaults to normal key for status
|
||||
* @returns Whether the status effect phase was successfully created.
|
||||
* @see {@linkcode doSetStatus} - alternate function that sets status immediately (albeit without condition checks).
|
||||
*/
|
||||
trySetStatus(
|
||||
effect?: StatusEffect,
|
||||
asPhase = false,
|
||||
effect: StatusEffect,
|
||||
sourcePokemon: Pokemon | null = null,
|
||||
turnsRemaining = 0,
|
||||
sleepTurnsRemaining?: number,
|
||||
sourceText: string | null = null,
|
||||
overrideStatus?: boolean,
|
||||
quiet = true,
|
||||
overrideMessage?: string | undefined,
|
||||
): boolean {
|
||||
// TODO: This needs to propagate failure status for non-status moves
|
||||
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
|
||||
return false;
|
||||
}
|
||||
@ -4742,49 +4746,74 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
}
|
||||
|
||||
if (asPhase) {
|
||||
if (overrideStatus) {
|
||||
this.resetStatus(false);
|
||||
}
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"ObtainStatusEffectPhase",
|
||||
this.getBattlerIndex(),
|
||||
effect,
|
||||
turnsRemaining,
|
||||
sourceText,
|
||||
sourcePokemon,
|
||||
);
|
||||
return true;
|
||||
if (overrideStatus) {
|
||||
this.resetStatus(false);
|
||||
}
|
||||
|
||||
let sleepTurnsRemaining: NumberHolder;
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"ObtainStatusEffectPhase",
|
||||
this.getBattlerIndex(),
|
||||
effect,
|
||||
sourcePokemon,
|
||||
sleepTurnsRemaining,
|
||||
sourceText,
|
||||
overrideMessage,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | status condition} to the specified effect.
|
||||
* Does **NOT** perform any feasibility checks whatsoever; must be checked by the caller.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
*/
|
||||
doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | status condition} to the specified effect.
|
||||
* Does **NOT** perform any feasibility checks whatsoever; must be checked by the caller.
|
||||
* @param effect - {@linkcode 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;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | status condition} to the specified effect.
|
||||
* Does **NOT** perform any feasibility checks whatsoever; must be checked by the caller.
|
||||
* @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
|
||||
* and is unused for all non-sleep Statuses.
|
||||
*/
|
||||
doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | status condition} to the specified effect.
|
||||
* Does **NOT** perform any feasibility checks whatsoever; must be checked by the caller.
|
||||
* @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
|
||||
* and is unused for all non-sleep Statuses.
|
||||
*/
|
||||
doSetStatus(
|
||||
effect: StatusEffect,
|
||||
sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : 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 semi-invulnerable when put asleep (such as due to Yawm),
|
||||
// remove their invulnerability and cancel the upcoming move from the queue
|
||||
const invulnTagTypes = [
|
||||
BattlerTagType.FLYING,
|
||||
BattlerTagType.UNDERGROUND,
|
||||
BattlerTagType.UNDERWATER,
|
||||
BattlerTagType.HIDDEN,
|
||||
BattlerTagType.FLYING,
|
||||
];
|
||||
|
||||
const tag = invulnerableTags.find(t => this.getTag(t));
|
||||
|
||||
if (tag) {
|
||||
this.removeTag(tag);
|
||||
this.getMoveQueue().pop();
|
||||
if (this.findTag(t => invulnTagTypes.includes(t.tagType))) {
|
||||
this.findAndRemoveTags(t => invulnTagTypes.includes(t.tagType));
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1747,12 +1747,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, undefined, undefined, this.type.name);
|
||||
return pokemon.trySetStatus(this.effect, pokemon, undefined, this.type.name);
|
||||
}
|
||||
|
||||
getMaxHeldItemCount(_pokemon: Pokemon): number {
|
||||
@ -3618,7 +3618,7 @@ export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifi
|
||||
*/
|
||||
override apply(enemyPokemon: Pokemon): boolean {
|
||||
if (randSeedFloat() <= this.chance * this.getStackCount()) {
|
||||
return enemyPokemon.trySetStatus(this.effect, true);
|
||||
return enemyPokemon.trySetStatus(this.effect);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -264,7 +264,7 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||
const removePokemon = () => {
|
||||
globalScene.addFaintedEnemyScore(pokemon);
|
||||
pokemon.hp = 0;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||
globalScene.clearEnemyHeldItemModifiers();
|
||||
pokemon.leaveField(true, true, true);
|
||||
};
|
||||
|
@ -49,7 +49,7 @@ export class AttemptRunPhase extends PokemonPhase {
|
||||
enemyField.forEach(enemyPokemon => {
|
||||
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
|
||||
enemyPokemon.hp = 0;
|
||||
enemyPokemon.trySetStatus(StatusEffect.FAINT);
|
||||
enemyPokemon.doSetStatus(StatusEffect.FAINT);
|
||||
});
|
||||
|
||||
globalScene.phaseManager.pushNew("BattleEndPhase", false);
|
||||
|
@ -209,7 +209,7 @@ export class FaintPhase extends PokemonPhase {
|
||||
pokemon.lapseTags(BattlerTagLapseType.FAINT);
|
||||
|
||||
pokemon.y -= 150;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||
if (pokemon.isPlayer()) {
|
||||
globalScene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon);
|
||||
} else {
|
||||
|
@ -272,8 +272,8 @@ export class MovePhase extends BattlePhase {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
this.pokemon.resetStatus();
|
||||
this.pokemon.updateInfo();
|
||||
// cannot use `asPhase=true` as it will cause status to be reset _after_ move condition checks fire
|
||||
this.pokemon.resetStatus(false, false, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,68 +2,60 @@ import { globalScene } from "#app/global-scene";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { CommonBattleAnim } from "#app/data/battle-anims";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { getStatusEffectObtainText, getStatusEffectOverlapText } from "#app/data/status-effect";
|
||||
import { getStatusEffectObtainText } from "#app/data/status-effect";
|
||||
import { StatusEffect } from "#app/enums/status-effect";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms/form-change-triggers";
|
||||
import { applyPostSetStatusAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
|
||||
/** The phase where pokemon obtain status effects. */
|
||||
export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||
public readonly phaseName = "ObtainStatusEffectPhase";
|
||||
private statusEffect?: StatusEffect;
|
||||
private turnsRemaining?: number;
|
||||
private sourceText?: string | null;
|
||||
private sourcePokemon?: Pokemon | null;
|
||||
|
||||
/**
|
||||
* Create a new ObtainStatusEffectPhase.
|
||||
* @param battlerIndex - The {@linkcode BattlerIndex} of the Pokemon obtaining the status effect.
|
||||
* @param statusEffect - The {@linkcode StatusEffect} being applied.
|
||||
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`.
|
||||
* @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for;
|
||||
* defaults to a random number between 2 and 4 and is unused for non-Sleep statuses.
|
||||
* @param sourceText - The text to show for the source of the status effect, if any; default `null`.
|
||||
* @param overrideMessage - A string containing text to be displayed upon status setting;
|
||||
* defaults to normal key for status if blank or `undefined`.
|
||||
*/
|
||||
constructor(
|
||||
battlerIndex: BattlerIndex,
|
||||
statusEffect?: StatusEffect,
|
||||
turnsRemaining?: number,
|
||||
sourceText?: string | null,
|
||||
sourcePokemon?: Pokemon | null,
|
||||
private statusEffect: StatusEffect,
|
||||
private sourcePokemon?: Pokemon | null,
|
||||
private sleepTurnsRemaining?: number,
|
||||
private sourceText?: string | null,
|
||||
private overrideMessage?: string | undefined,
|
||||
) {
|
||||
super(battlerIndex);
|
||||
|
||||
this.statusEffect = statusEffect;
|
||||
this.turnsRemaining = turnsRemaining;
|
||||
this.sourceText = sourceText;
|
||||
this.sourcePokemon = sourcePokemon;
|
||||
}
|
||||
|
||||
start() {
|
||||
const pokemon = this.getPokemon();
|
||||
if (pokemon && !pokemon.status) {
|
||||
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
|
||||
if (this.turnsRemaining) {
|
||||
pokemon.status!.sleepTurnsRemaining = this.turnsRemaining;
|
||||
}
|
||||
pokemon.updateInfo(true);
|
||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectObtainText(
|
||||
this.statusEffect,
|
||||
getPokemonNameWithAffix(pokemon),
|
||||
this.sourceText ?? undefined,
|
||||
),
|
||||
);
|
||||
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
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
applyPostSetStatusAbAttrs("PostSetStatusAbAttr", pokemon, this.statusEffect, this.sourcePokemon);
|
||||
}
|
||||
this.end();
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (pokemon.status?.effect === this.statusEffect) {
|
||||
|
||||
pokemon.doSetStatus(this.statusEffect, this.sleepTurnsRemaining);
|
||||
pokemon.updateInfo(true);
|
||||
|
||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(false, () => {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)),
|
||||
this.overrideMessage ||
|
||||
getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined),
|
||||
);
|
||||
}
|
||||
this.end();
|
||||
if (this.statusEffect && this.statusEffect !== StatusEffect.FAINT) {
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true);
|
||||
// If the status was applied from a move, ensure abilities are not ignored for follow-up triggers.
|
||||
// TODO: Ensure this isn't breaking any other phases unshifted afterwards
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
applyPostSetStatusAbAttrs("PostSetStatusAbAttr", pokemon, this.statusEffect, this.sourcePokemon);
|
||||
}
|
||||
this.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { CommonAnimPhase } from "./common-anim-phase";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import type { HealBlockTag } from "#app/data/battler-tags";
|
||||
|
||||
// TODO: Refactor this - it has far too many arguments
|
||||
export class PokemonHealPhase extends CommonAnimPhase {
|
||||
public readonly phaseName = "PokemonHealPhase";
|
||||
private hpHealed: number;
|
||||
@ -28,7 +29,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
||||
battlerIndex: BattlerIndex,
|
||||
hpHealed: number,
|
||||
message: string | null,
|
||||
showFullHpMessage: boolean,
|
||||
showFullHpMessage = true,
|
||||
skipAnim = false,
|
||||
revive = false,
|
||||
healStatus = false,
|
||||
@ -69,9 +70,10 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
||||
|
||||
if (healBlock && this.hpHealed > 0) {
|
||||
globalScene.phaseManager.queueMessage(healBlock.onActivation(pokemon));
|
||||
this.message = null;
|
||||
return super.end();
|
||||
super.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (healOrDamage) {
|
||||
const hpRestoreMultiplier = new NumberHolder(1);
|
||||
if (!this.revive) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -22,25 +23,66 @@ describe("Abilities - Corrosion", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.GRIMER)
|
||||
.enemyAbility(AbilityId.CORROSION)
|
||||
.enemyMoveset(MoveId.TOXIC);
|
||||
.ability(AbilityId.CORROSION)
|
||||
.enemyAbility(AbilityId.NO_GUARD)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => {
|
||||
game.override.ability(AbilityId.SYNCHRONIZE);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
it.each<{ name: string; species: SpeciesId }>([
|
||||
{ name: "Poison", species: SpeciesId.GRIMER },
|
||||
{ name: "Steel", species: SpeciesId.KLINK },
|
||||
])("should grant the user the ability to poison $name-type opponents", async ({ species }) => {
|
||||
game.override.enemySpecies(species);
|
||||
await game.classicMode.startBattle([SpeciesId.SALANDIT]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(playerPokemon!.status).toBeUndefined();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.status?.effect).toBeUndefined();
|
||||
|
||||
game.move.use(MoveId.POISON_GAS);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.POISON);
|
||||
});
|
||||
|
||||
it("should not affect Toxic Spikes", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SALANDIT]);
|
||||
|
||||
game.move.use(MoveId.TOXIC_SPIKES);
|
||||
await game.doKillOpponents();
|
||||
await game.toNextWave();
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
expect(enemyPokemon.status).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not affect an opponent's Synchronize ability", async () => {
|
||||
game.override.enemyAbility(AbilityId.SYNCHRONIZE);
|
||||
await game.classicMode.startBattle([SpeciesId.ARBOK]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
expect(enemyPokemon.status?.effect).toBeUndefined();
|
||||
|
||||
game.move.use(MoveId.TOXIC);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemyPokemon.status?.effect).toBe(StatusEffect.TOXIC);
|
||||
expect(playerPokemon.status?.effect).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should affect the user's held Toxic Orb", async () => {
|
||||
game.override.startingHeldItems([{ name: "TOXIC_ORB", count: 1 }]);
|
||||
await game.classicMode.startBattle([SpeciesId.SALAZZLE]);
|
||||
|
||||
const salazzle = game.field.getPlayerPokemon();
|
||||
expect(salazzle.status?.effect).toBeUndefined();
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(playerPokemon!.status).toBeDefined();
|
||||
expect(enemyPokemon!.status).toBeUndefined();
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(salazzle.status?.effect).toBe(StatusEffect.TOXIC);
|
||||
});
|
||||
});
|
||||
|
@ -49,6 +49,7 @@ describe("Abilities - Healer", () => {
|
||||
const user = game.scene.getPlayerPokemon()!;
|
||||
// Only want one magikarp to have the ability
|
||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
// faint the ally
|
||||
game.move.select(MoveId.LUNAR_DANCE, 1);
|
||||
@ -62,9 +63,10 @@ describe("Abilities - Healer", () => {
|
||||
it("should heal the status of an ally if the ally has a status", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||
const [user, ally] = game.scene.getPlayerField();
|
||||
|
||||
// Only want one magikarp to have the ability.
|
||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||
expect(ally.trySetStatus(StatusEffect.BURN)).toBe(true);
|
||||
ally.doSetStatus(StatusEffect.BURN);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
|
||||
@ -80,7 +82,7 @@ describe("Abilities - Healer", () => {
|
||||
const [user, ally] = game.scene.getPlayerField();
|
||||
// Only want one magikarp to have the ability.
|
||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||
expect(ally.trySetStatus(StatusEffect.BURN)).toBe(true);
|
||||
ally.doSetStatus(StatusEffect.BURN);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Insomnia", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove sleep when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.INSOMNIA)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.SLEEP);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Limber", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove paralysis when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.LIMBER)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.PARALYSIS);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.PARALYSIS);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Magma Armor", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove freeze when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.MAGMA_ARMOR)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.FREEZE);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.FREEZE);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
95
test/abilities/status-immunity-ab-attrs.test.ts
Normal file
95
test/abilities/status-immunity-ab-attrs.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { StatusEffectAttr } from "#app/data/moves/move";
|
||||
import { toReadableString } from "#app/utils/common";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe.each<{ name: string; ability: AbilityId; status: StatusEffect }>([
|
||||
{ name: "Vital Spirit", ability: AbilityId.VITAL_SPIRIT, status: StatusEffect.SLEEP },
|
||||
{ name: "Insomnia", ability: AbilityId.INSOMNIA, status: StatusEffect.SLEEP },
|
||||
{ name: "Immunity", ability: AbilityId.IMMUNITY, status: StatusEffect.POISON },
|
||||
{ name: "Magma Armor", ability: AbilityId.MAGMA_ARMOR, status: StatusEffect.FREEZE },
|
||||
{ name: "Limber", ability: AbilityId.LIMBER, status: StatusEffect.PARALYSIS },
|
||||
{ name: "Thermal Exchange", ability: AbilityId.THERMAL_EXCHANGE, status: StatusEffect.BURN },
|
||||
{ name: "Water Veil", ability: AbilityId.WATER_VEIL, status: StatusEffect.BURN },
|
||||
{ name: "Water Bubble", ability: AbilityId.WATER_BUBBLE, status: StatusEffect.BURN },
|
||||
])("Abilities - $name", ({ ability, status }) => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(ability)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
|
||||
// Mock Lumina Crash and Spore to be our status-inflicting moves of choice
|
||||
vi.spyOn(allMoves[MoveId.LUMINA_CRASH], "attrs", "get").mockReturnValue([new StatusEffectAttr(status, false)]);
|
||||
vi.spyOn(allMoves[MoveId.SPORE], "attrs", "get").mockReturnValue([new StatusEffectAttr(status, false)]);
|
||||
});
|
||||
|
||||
const statusStr = toReadableString(StatusEffect[status]);
|
||||
|
||||
it(`should prevent application of ${statusStr} without failing damaging moves`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const karp = game.field.getEnemyPokemon();
|
||||
expect(karp.status?.effect).toBeUndefined();
|
||||
expect(karp.canSetStatus(status)).toBe(false);
|
||||
|
||||
game.move.use(MoveId.LUMINA_CRASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(karp.status?.effect).toBeUndefined();
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it(`should cure ${statusStr} upon being gained`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
feebas.doSetStatus(status);
|
||||
expect(feebas.status?.effect).toBe(status);
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.SKILL_SWAP); // need to force enemy to use it as
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(feebas.status?.effect).toBeUndefined();
|
||||
});
|
||||
|
||||
// TODO: This does not propagate failures currently
|
||||
it.todo(
|
||||
`should cause status moves inflicting ${statusStr} to count as failed if no other effects can be applied`,
|
||||
async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const karp = game.field.getEnemyPokemon();
|
||||
expect(karp.status?.effect).toBeUndefined();
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
},
|
||||
);
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Thermal Exchange", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove burn when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.BURN);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Vital Spirit", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove sleep when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.INSOMNIA)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.SLEEP);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Water Bubble", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove burn when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.BURN);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Water Veil", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove burn when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.BURN);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,12 +1,11 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
|
||||
describe("Abilities - Immunity", () => {
|
||||
describe("Spec - Pokemon Functions", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
@ -23,29 +22,29 @@ describe("Abilities - Immunity", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.startingLevel(100)
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove poison when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.IMMUNITY)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.POISON);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.POISON);
|
||||
describe("doSetStatus", () => {
|
||||
it("should change the Pokemon's status, ignoring feasibility checks", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ACCELGOR]);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
expect(player.status?.effect).toBeUndefined();
|
||||
player.doSetStatus(StatusEffect.BURN);
|
||||
expect(player.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
expect(player.canSetStatus(StatusEffect.SLEEP)).toBe(false);
|
||||
player.doSetStatus(StatusEffect.SLEEP, 5);
|
||||
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(player.status?.sleepTurnsRemaining).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
@ -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([SpeciesId.ABRA]);
|
||||
|
||||
const pkm = game.scene.getPlayerPokemon()!;
|
||||
expect(pkm).toBeDefined();
|
||||
|
||||
expect(pkm.trySetStatus(undefined)).toBe(true);
|
||||
});
|
||||
|
||||
describe("Add To Party", () => {
|
||||
let scene: BattleScene;
|
||||
|
||||
|
@ -73,7 +73,7 @@ describe("Moves - Beat Up", () => {
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.scene.getPlayerParty()[1].trySetStatus(StatusEffect.BURN);
|
||||
game.scene.getPlayerParty()[1].doSetStatus(StatusEffect.BURN);
|
||||
|
||||
game.move.select(MoveId.BEAT_UP);
|
||||
|
||||
|
@ -44,7 +44,7 @@ describe("Moves - Fusion Flare", () => {
|
||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||
|
||||
// Inflict freeze quietly and check if it was properly inflicted
|
||||
partyMember.trySetStatus(StatusEffect.FREEZE, false);
|
||||
partyMember.doSetStatus(StatusEffect.FREEZE);
|
||||
expect(partyMember.status!.effect).toBe(StatusEffect.FREEZE);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
146
test/moves/rest.test.ts
Normal file
146
test/moves/rest.test.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Move - Rest", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.EKANS)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
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([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.POISON);
|
||||
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(snorlax.hp).toBe(snorlax.getMaxHp());
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
});
|
||||
|
||||
it("should always last 3 turns", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
|
||||
// Cf https://bulbapedia.bulbagarden.net/wiki/Rest_(move):
|
||||
// > The user is unable to use MoveId while asleep for 2 turns after the turn when Rest is used.
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(snorlax.status?.sleepTurnsRemaining).toBe(3);
|
||||
|
||||
game.move.use(MoveId.SWORDS_DANCE);
|
||||
await game.toNextTurn();
|
||||
expect(snorlax.status?.sleepTurnsRemaining).toBe(2);
|
||||
|
||||
game.move.use(MoveId.SWORDS_DANCE);
|
||||
await game.toNextTurn();
|
||||
expect(snorlax.status?.sleepTurnsRemaining).toBe(1);
|
||||
|
||||
game.move.use(MoveId.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([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
snorlax.addTag(BattlerTagType.CONFUSED, 999);
|
||||
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(snorlax.getTag(BattlerTagType.CONFUSED)).toBeDefined();
|
||||
});
|
||||
|
||||
it.each<{ name: string; status?: StatusEffect; ability?: AbilityId; dmg?: number }>([
|
||||
{ name: "is at full HP", dmg: 0 },
|
||||
{ name: "is grounded on Electric Terrain", ability: AbilityId.ELECTRIC_SURGE },
|
||||
{ name: "is grounded on Misty Terrain", ability: AbilityId.MISTY_SURGE },
|
||||
{ name: "has Comatose", ability: AbilityId.COMATOSE },
|
||||
])("should fail if the user $name", async ({ status = StatusEffect.NONE, ability = AbilityId.NONE, dmg = 1 }) => {
|
||||
game.override.ability(ability).statusEffect(status);
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
|
||||
snorlax.hp = snorlax.getMaxHp() - dmg;
|
||||
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail if called while already asleep", async () => {
|
||||
game.override.statusEffect(StatusEffect.SLEEP).moveset([MoveId.REST, MoveId.SLEEP_TALK]);
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
|
||||
// Need to use sleep talk here since you normally can't move while asleep
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(snorlax.isFullHp()).toBe(false);
|
||||
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 same turn as the user wakes", async () => {
|
||||
game.override.statusEffect(StatusEffect.SLEEP);
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
snorlax.status!.sleepTurnsRemaining = 1;
|
||||
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(snorlax.status!.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(snorlax.isFullHp()).toBe(true);
|
||||
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(snorlax.status!.sleepTurnsRemaining).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
@ -7,6 +7,7 @@ import { SpeciesId } from "#enums/species-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
describe("Moves - Sleep Talk", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -31,45 +32,73 @@ describe("Moves - Sleep Talk", () => {
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyAbility(AbilityId.NO_GUARD)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it("should fail when the user is not asleep", async () => {
|
||||
game.override.statusEffect(StatusEffect.NONE);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail if the user has no valid moves", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.METRONOME, MoveId.SOLAR_BEAM]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should call a random valid move if the user is asleep", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK));
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas.getStatStage(Stat.ATK)).toBe(2);
|
||||
expect(feebas.getLastXMoves(2)).toEqual([
|
||||
expect.objectContaining({
|
||||
move: MoveId.SWORDS_DANCE,
|
||||
result: MoveResult.SUCCESS,
|
||||
useMode: MoveUseMode.FOLLOW_UP,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
move: MoveId.SLEEP_TALK,
|
||||
result: MoveResult.SUCCESS,
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should apply secondary effects of a move", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.WOOD_HAMMER]); // Dig and Fly are invalid moves, Wood Hammer should always be called
|
||||
await game.classicMode.startBattle();
|
||||
it("should fail if the user is not asleep", async () => {
|
||||
game.override.statusEffect(StatusEffect.POISON);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail the turn the user wakes up from Sleep", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
feebas.status!.sleepTurnsRemaining = 1;
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail if the user has no valid moves", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.METRONOME, MoveId.SOLAR_BEAM]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should apply secondary effects of the called move", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.SCALE_SHOT]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // Wood Hammer recoil effect should be applied
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas.getStatStage(Stat.SPD)).toBe(1);
|
||||
expect(feebas.getStatStage(Stat.DEF)).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
@ -63,7 +63,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
// Both pokemon fainted
|
||||
scene.getPlayerParty().forEach(p => {
|
||||
p.hp = 0;
|
||||
p.trySetStatus(StatusEffect.FAINT);
|
||||
p.doSetStatus(StatusEffect.FAINT);
|
||||
void p.updateInfo();
|
||||
});
|
||||
|
||||
@ -83,7 +83,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
@ -102,7 +102,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
@ -121,7 +121,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
@ -167,7 +167,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].level = 100;
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
party[1].level = 10;
|
||||
|
||||
@ -206,7 +206,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].level = 10;
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
party[1].level = 100;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user