mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-21 14:59:26 +02:00
Merge branch 'beta' into doTheAchievementShuffle
This commit is contained in:
commit
333e80d172
@ -2,7 +2,7 @@ import type { ArenaTagTypeMap } from "#data/arena-tag";
|
|||||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
|
||||||
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
|
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
|
||||||
export type ArenaTrapTagType =
|
export type EntryHazardTagType =
|
||||||
| ArenaTagType.STICKY_WEB
|
| ArenaTagType.STICKY_WEB
|
||||||
| ArenaTagType.SPIKES
|
| ArenaTagType.SPIKES
|
||||||
| ArenaTagType.TOXIC_SPIKES
|
| ArenaTagType.TOXIC_SPIKES
|
||||||
|
@ -6,7 +6,7 @@ import type { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-chang
|
|||||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#data/arena-tag";
|
import type { EntryHazardTag, SuppressAbilitiesTag } from "#data/arena-tag";
|
||||||
import type { BattlerTag } from "#data/battler-tags";
|
import type { BattlerTag } from "#data/battler-tags";
|
||||||
import { GroundedTag } from "#data/battler-tags";
|
import { GroundedTag } from "#data/battler-tags";
|
||||||
import { getBerryEffectFunc } from "#data/berry";
|
import { getBerryEffectFunc } from "#data/berry";
|
||||||
@ -1113,7 +1113,7 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean {
|
override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean {
|
||||||
const tag = globalScene.arena.getTag(this.arenaTagType) as ArenaTrapTag;
|
const tag = globalScene.arena.getTag(this.arenaTagType) as EntryHazardTag;
|
||||||
return (
|
return (
|
||||||
this.condition(pokemon, attacker, move) &&
|
this.condition(pokemon, attacker, move) &&
|
||||||
(!globalScene.arena.getTag(this.arenaTagType) || tag.layers < tag.maxLayers)
|
(!globalScene.arena.getTag(this.arenaTagType) || tag.layers < tag.maxLayers)
|
||||||
@ -1235,7 +1235,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
|
|||||||
// TODO: Probably want to check against simulated here
|
// TODO: Probably want to check against simulated here
|
||||||
const effect =
|
const effect =
|
||||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||||
attacker.trySetStatus(effect, true, pokemon);
|
attacker.trySetStatus(effect, pokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2228,7 +2228,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
|
|||||||
apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
|
apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
|
||||||
const effect =
|
const effect =
|
||||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||||
opponent.trySetStatus(effect, true, pokemon);
|
opponent.trySetStatus(effect, pokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2383,7 +2383,7 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr {
|
|||||||
*/
|
*/
|
||||||
override apply({ simulated, effect, sourcePokemon, pokemon }: PostSetStatusAbAttrParams): void {
|
override apply({ simulated, effect, sourcePokemon, pokemon }: PostSetStatusAbAttrParams): void {
|
||||||
if (!simulated && sourcePokemon) {
|
if (!simulated && sourcePokemon) {
|
||||||
sourcePokemon.trySetStatus(effect, true, pokemon);
|
sourcePokemon.trySetStatus(effect, pokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3659,7 +3659,8 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
|||||||
protected immuneEffects: StatusEffect[];
|
protected immuneEffects: StatusEffect[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param immuneEffects - The status effects to which the Pokémon is immune.
|
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||||
|
* If none are provided, will block **all** status effects regardless of type.
|
||||||
*/
|
*/
|
||||||
constructor(...immuneEffects: StatusEffect[]) {
|
constructor(...immuneEffects: StatusEffect[]) {
|
||||||
super();
|
super();
|
||||||
@ -3668,7 +3669,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
|
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
|
||||||
return (effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) || this.immuneEffects.includes(effect);
|
return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3720,6 +3721,11 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar
|
|||||||
*/
|
*/
|
||||||
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||||
protected immuneEffects: StatusEffect[];
|
protected immuneEffects: StatusEffect[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||||
|
* If none are provided, will block **all** status effects regardless of type.
|
||||||
|
*/
|
||||||
constructor(...immuneEffects: StatusEffect[]) {
|
constructor(...immuneEffects: StatusEffect[]) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -3728,7 +3734,7 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
|||||||
|
|
||||||
override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean {
|
override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean {
|
||||||
return (
|
return (
|
||||||
(!cancelled.value && effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) ||
|
(!cancelled.value && this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) ||
|
||||||
this.immuneEffects.includes(effect)
|
this.immuneEffects.includes(effect)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -3754,6 +3760,10 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
|
|||||||
*/
|
*/
|
||||||
private condition: (target: Pokemon, source: Pokemon | null) => boolean;
|
private condition: (target: Pokemon, source: Pokemon | null) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||||
|
* If none are provided, will block **all** status effects regardless of type.
|
||||||
|
*/
|
||||||
constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) {
|
constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) {
|
||||||
super(...immuneEffects);
|
super(...immuneEffects);
|
||||||
|
|
||||||
@ -7481,8 +7491,7 @@ export function initAbilities() {
|
|||||||
.unsuppressable()
|
.unsuppressable()
|
||||||
.bypassFaint(),
|
.bypassFaint(),
|
||||||
new Ability(AbilityId.CORROSION, 7)
|
new Ability(AbilityId.CORROSION, 7)
|
||||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ])
|
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]),
|
||||||
.edgeCase(), // Should poison itself with toxic orb.
|
|
||||||
new Ability(AbilityId.COMATOSE, 7)
|
new Ability(AbilityId.COMATOSE, 7)
|
||||||
.attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects())
|
.attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects())
|
||||||
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||||
|
@ -24,11 +24,11 @@ import type { Pokemon } from "#field/pokemon";
|
|||||||
import type {
|
import type {
|
||||||
ArenaScreenTagType,
|
ArenaScreenTagType,
|
||||||
ArenaTagTypeData,
|
ArenaTagTypeData,
|
||||||
ArenaTrapTagType,
|
EntryHazardTagType,
|
||||||
SerializableArenaTagType,
|
SerializableArenaTagType,
|
||||||
} from "#types/arena-tags";
|
} from "#types/arena-tags";
|
||||||
import type { Mutable } from "#types/type-helpers";
|
import type { Mutable } from "#types/type-helpers";
|
||||||
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
|
import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -725,42 +725,79 @@ export class IonDelugeTag extends ArenaTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class to implement arena traps.
|
* Abstract class to implement [entry hazards](https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards).
|
||||||
|
* These persistent tags remain on-field across turns and apply effects to any {@linkcode Pokemon} switching in. \
|
||||||
|
* Uniquely, adding a tag multiple times may stack multiple "layers" of the effect, increasing its severity.
|
||||||
*/
|
*/
|
||||||
export abstract class ArenaTrapTag extends SerializableArenaTag {
|
export abstract class EntryHazardTag extends SerializableArenaTag {
|
||||||
abstract readonly tagType: ArenaTrapTagType;
|
public declare abstract readonly tagType: EntryHazardTagType;
|
||||||
public layers: number;
|
|
||||||
public maxLayers: number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the ArenaTrapTag class.
|
* The current number of layers this tag has.
|
||||||
*
|
* Starts at 1 and increases each time the trap is laid.
|
||||||
* @param tagType - The type of the arena tag.
|
|
||||||
* @param sourceMove - The move that created the tag.
|
|
||||||
* @param sourceId - The ID of the source of the tag.
|
|
||||||
* @param side - The side (player or enemy) the tag affects.
|
|
||||||
* @param maxLayers - The maximum amount of layers this tag can have.
|
|
||||||
*/
|
*/
|
||||||
constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide, maxLayers: number) {
|
public layers = 1;
|
||||||
super(0, sourceMove, sourceId, side);
|
/** The maximum number of layers this tag can have. */
|
||||||
|
public abstract get maxLayers(): number;
|
||||||
this.layers = 1;
|
/** Whether this tag should only affect grounded targets; default `true` */
|
||||||
this.maxLayers = maxLayers;
|
protected get groundedOnly(): boolean {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onOverlap(arena: Arena, _source: Pokemon | null): void {
|
constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide) {
|
||||||
if (this.layers < this.maxLayers) {
|
super(0, sourceMove, sourceId, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add a `canAdd` field to arena tags to remove need for callers to check layer counts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display text when this tag is added to the field.
|
||||||
|
* @param _arena - The {@linkcode Arena} at the time of adding this tag
|
||||||
|
* @param quiet - Whether to suppress messages during tag creation; default `false`
|
||||||
|
*/
|
||||||
|
override onAdd(_arena: Arena, quiet = false): void {
|
||||||
|
// Here, `quiet=true` means "just add the tag, no questions asked"
|
||||||
|
if (quiet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = this.getSourcePokemon();
|
||||||
|
if (!source) {
|
||||||
|
console.warn(
|
||||||
|
"Failed to get source Pokemon for AernaTrapTag on add message!" +
|
||||||
|
`\nTag type: ${this.tagType}` +
|
||||||
|
`\nPID: ${this.sourceId}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScene.phaseManager.queueMessage(this.getAddMessage(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the text to be displayed upon adding a new layer to this trap.
|
||||||
|
* @param source - The {@linkcode Pokemon} having created this tag
|
||||||
|
* @returns The localized message to be displayed on screen.
|
||||||
|
*/
|
||||||
|
protected abstract getAddMessage(source: Pokemon): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new layer to this tag upon overlap, triggering the tag's normal {@linkcode onAdd} effects upon doing so.
|
||||||
|
* @param arena - The {@linkcode arena} at the time of adding the tag
|
||||||
|
*/
|
||||||
|
override onOverlap(arena: Arena): void {
|
||||||
|
if (this.layers >= this.maxLayers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.layers++;
|
this.layers++;
|
||||||
|
|
||||||
this.onAdd(arena);
|
this.onAdd(arena);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates the hazard effect onto a Pokemon when it enters the field
|
* Activate the hazard effect onto a Pokemon when it enters the field.
|
||||||
* @param _arena the {@linkcode Arena} containing this tag
|
* @param _arena - The {@linkcode Arena} at the time of tag activation
|
||||||
* @param simulated if `true`, only checks if the hazard would activate.
|
* @param simulated - Whether to suppress activation effects during execution
|
||||||
* @param pokemon the {@linkcode Pokemon} triggering this hazard
|
* @param pokemon - The {@linkcode Pokemon} triggering this hazard
|
||||||
* @returns `true` if this hazard affects the given Pokemon; `false` otherwise.
|
* @returns `true` if this hazard affects the given Pokemon; `false` otherwise.
|
||||||
*/
|
*/
|
||||||
override apply(_arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
|
override apply(_arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
|
||||||
@ -768,12 +805,21 @@ export abstract class ArenaTrapTag extends SerializableArenaTag {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.groundedOnly && !pokemon.isGrounded()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return this.activateTrap(pokemon, simulated);
|
return this.activateTrap(pokemon, simulated);
|
||||||
}
|
}
|
||||||
|
|
||||||
activateTrap(_pokemon: Pokemon, _simulated: boolean): boolean {
|
/**
|
||||||
return false;
|
* Activate this trap's effects when a Pokemon switches into it.
|
||||||
}
|
* @param _pokemon - The {@linkcode Pokemon}
|
||||||
|
* @param _simulated - Whether the activation is simulated
|
||||||
|
* @returns Whether the trap activation succeeded
|
||||||
|
* @todo Do we need the return value? nothing uses it
|
||||||
|
*/
|
||||||
|
protected abstract activateTrap(_pokemon: Pokemon, _simulated: boolean): boolean;
|
||||||
|
|
||||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||||
return pokemon.isGrounded()
|
return pokemon.isGrounded()
|
||||||
@ -781,122 +827,174 @@ export abstract class ArenaTrapTag extends SerializableArenaTag {
|
|||||||
: Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
|
: Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadTag<T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "layers" | "maxLayers">): void {
|
public loadTag<T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "layers">): void {
|
||||||
super.loadTag(source);
|
super.loadTag(source);
|
||||||
this.layers = source.layers;
|
this.layers = source.layers;
|
||||||
this.maxLayers = source.maxLayers;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class to implement damaging entry hazards.
|
||||||
|
* Currently used for {@linkcode SpikesTag} and {@linkcode StealthRockTag}.
|
||||||
|
*/
|
||||||
|
abstract class DamagingTrapTag extends EntryHazardTag {
|
||||||
|
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||||
|
// Check for magic guard immunity
|
||||||
|
const cancelled = new BooleanHolder(false);
|
||||||
|
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||||
|
if (cancelled.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simulated) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Damage the target and trigger a message
|
||||||
|
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||||
|
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||||
|
|
||||||
|
globalScene.phaseManager.queueMessage(this.getTriggerMessage(pokemon));
|
||||||
|
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||||
|
pokemon.turnData.damageTaken += damage;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the text to be displayed when this tag deals damage.
|
||||||
|
* @param _pokemon - The {@linkcode Pokemon} switching in
|
||||||
|
* @returns The localized trigger message to be displayed on-screen.
|
||||||
|
*/
|
||||||
|
protected abstract getTriggerMessage(_pokemon: Pokemon): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the amount of damage this tag should deal to the given Pokemon, relative to its maximum HP.
|
||||||
|
* @param _pokemon - The {@linkcode Pokemon} switching in
|
||||||
|
* @returns The percentage of max HP to deal upon activation.
|
||||||
|
*/
|
||||||
|
protected abstract getDamageHpRatio(_pokemon: Pokemon): number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Spikes_(move) Spikes}.
|
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Spikes_(move) Spikes}.
|
||||||
* Applies up to 3 layers of Spikes, dealing 1/8th, 1/6th, or 1/4th of the the Pokémon's HP
|
* Applies up to 3 layers of Spikes, dealing 1/8th, 1/6th, or 1/4th of the the Pokémon's HP
|
||||||
* in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap.
|
* in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap.
|
||||||
*/
|
*/
|
||||||
class SpikesTag extends ArenaTrapTag {
|
class SpikesTag extends DamagingTrapTag {
|
||||||
public readonly tagType = ArenaTagType.SPIKES;
|
public readonly tagType = ArenaTagType.SPIKES;
|
||||||
|
override get maxLayers() {
|
||||||
|
return 3 as const;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||||
super(MoveId.SPIKES, sourceId, side, 3);
|
super(MoveId.SPIKES, sourceId, side);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(arena: Arena, quiet = false): void {
|
protected override getAddMessage(source: Pokemon): string {
|
||||||
super.onAdd(arena);
|
return i18next.t("arenaTag:spikesOnAdd", {
|
||||||
|
|
||||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
|
||||||
if (quiet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = this.getSourcePokemon();
|
|
||||||
if (!source) {
|
|
||||||
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("arenaTag:spikesOnAdd", {
|
|
||||||
moveName: this.getMoveName(),
|
moveName: this.getMoveName(),
|
||||||
opponentDesc: source.getOpponentDescriptor(),
|
opponentDesc: source.getOpponentDescriptor(),
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
protected override getTriggerMessage(pokemon: Pokemon): string {
|
||||||
if (!pokemon.isGrounded()) {
|
return i18next.t("arenaTag:spikesActivateTrap", {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelled = new BooleanHolder(false);
|
|
||||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
|
||||||
if (simulated || cancelled.value) {
|
|
||||||
return !cancelled.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const damageHpRatio = 1 / (10 - 2 * this.layers);
|
|
||||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("arenaTag:spikesActivateTrap", {
|
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
}),
|
});
|
||||||
);
|
}
|
||||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
|
||||||
pokemon.turnData.damageTaken += damage;
|
protected override getDamageHpRatio(_pokemon: Pokemon): number {
|
||||||
return true;
|
// 1/8 for 1 layer, 1/6 for 2, 1/4 for 3
|
||||||
|
return 1 / (10 - 2 * this.layers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Toxic_Spikes_(move) Toxic Spikes}.
|
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) | Stealth Rock}.
|
||||||
* Applies up to 2 layers of Toxic Spikes, poisoning or badly poisoning any Pokémon who is
|
* Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon
|
||||||
* summoned into this trap if 1 or 2 layers of Toxic Spikes respectively are up. Poison-type
|
* who is summoned into the trap based on the Rock type's type effectiveness.
|
||||||
* Pokémon summoned into this trap remove it entirely.
|
|
||||||
*/
|
*/
|
||||||
class ToxicSpikesTag extends ArenaTrapTag {
|
class StealthRockTag extends DamagingTrapTag {
|
||||||
#neutralized: boolean;
|
public readonly tagType = ArenaTagType.STEALTH_ROCK;
|
||||||
public readonly tagType = ArenaTagType.TOXIC_SPIKES;
|
public override get maxLayers() {
|
||||||
|
return 1 as const;
|
||||||
|
}
|
||||||
|
protected override get groundedOnly() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||||
super(MoveId.TOXIC_SPIKES, sourceId, side, 2);
|
super(MoveId.STEALTH_ROCK, sourceId, side);
|
||||||
this.#neutralized = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(arena: Arena, quiet = false): void {
|
protected override getAddMessage(source: Pokemon): string {
|
||||||
super.onAdd(arena);
|
return i18next.t("arenaTag:stealthRockOnAdd", {
|
||||||
|
opponentDesc: source.getOpponentDescriptor(),
|
||||||
if (quiet) {
|
});
|
||||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = this.getSourcePokemon();
|
protected override getTriggerMessage(pokemon: Pokemon): string {
|
||||||
if (!source) {
|
return i18next.t("arenaTag:stealthRockActivateTrap", {
|
||||||
console.warn(`Failed to get source Pokemon for ToxicSpikesTag on add message; id: ${this.sourceId}`);
|
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
protected override getDamageHpRatio(pokemon: Pokemon): number {
|
||||||
i18next.t("arenaTag:toxicSpikesOnAdd", {
|
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true);
|
||||||
|
return 0.125 * effectiveness;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||||
|
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||||
|
return Phaser.Math.Linear(super.getMatchupScoreMultiplier(pokemon), 1, 1 - Math.pow(damageHpRatio, damageHpRatio));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Toxic_Spikes_(move) | Toxic Spikes}.
|
||||||
|
* Applies up to 2 layers of Toxic Spikes, poisoning or badly poisoning any Pokémon switched in
|
||||||
|
* based on the current layer count. \
|
||||||
|
* Poison-type Pokémon will remove it entirely upon switch-in.
|
||||||
|
*/
|
||||||
|
class ToxicSpikesTag extends EntryHazardTag {
|
||||||
|
/**
|
||||||
|
* Whether the tag is currently in the process of being neutralized by a Poison-type.
|
||||||
|
* @defaultValue `false`
|
||||||
|
*/
|
||||||
|
#neutralized = false;
|
||||||
|
public readonly tagType = ArenaTagType.TOXIC_SPIKES;
|
||||||
|
override get maxLayers() {
|
||||||
|
return 2 as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||||
|
super(MoveId.TOXIC_SPIKES, sourceId, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override getAddMessage(source: Pokemon): string {
|
||||||
|
return i18next.t("arenaTag:toxicSpikesOnAdd", {
|
||||||
moveName: this.getMoveName(),
|
moveName: this.getMoveName(),
|
||||||
opponentDesc: source.getOpponentDescriptor(),
|
opponentDesc: source.getOpponentDescriptor(),
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(arena: Arena): void {
|
// Override remove function to only display text when not neutralized
|
||||||
|
override onRemove(arena: Arena): void {
|
||||||
if (!this.#neutralized) {
|
if (!this.#neutralized) {
|
||||||
super.onRemove(arena);
|
super.onRemove(arena);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||||
if (pokemon.isGrounded()) {
|
|
||||||
if (simulated) {
|
if (simulated) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||||
|
// Neutralize the tag and remove it from the field.
|
||||||
|
// Message cannot be moved to `onRemove` as that requires a reference to the neutralizing pokemon
|
||||||
this.#neutralized = true;
|
this.#neutralized = true;
|
||||||
if (globalScene.arena.removeTag(this.tagType)) {
|
globalScene.arena.removeTagOnSide(this.tagType, this.side);
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
@ -905,17 +1003,10 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
|||||||
);
|
);
|
||||||
return true;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
// Attempt to poison the target, suppressing any status effect messages
|
||||||
|
const effect = this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC;
|
||||||
|
return pokemon.trySetStatus(effect, null, 0, this.getMoveName(), false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||||
@ -930,131 +1021,29 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) Stealth Rock}.
|
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Sticky_Web_(move) | Sticky Web}.
|
||||||
* Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon
|
* Applies a single-layer trap that lowers the Speed of all grounded Pokémon switching in.
|
||||||
* who is summoned into the trap, based on the Rock type's type effectiveness.
|
|
||||||
*/
|
*/
|
||||||
class StealthRockTag extends ArenaTrapTag {
|
class StickyWebTag extends EntryHazardTag {
|
||||||
public readonly tagType = ArenaTagType.STEALTH_ROCK;
|
|
||||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
|
||||||
super(MoveId.STEALTH_ROCK, sourceId, side, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(arena: Arena, quiet = false): void {
|
|
||||||
super.onAdd(arena);
|
|
||||||
|
|
||||||
if (quiet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = this.getSourcePokemon();
|
|
||||||
if (!quiet && source) {
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("arenaTag:stealthRockOnAdd", {
|
|
||||||
opponentDesc: source.getOpponentDescriptor(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDamageHpRatio(pokemon: Pokemon): number {
|
|
||||||
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true);
|
|
||||||
|
|
||||||
let damageHpRatio = 0;
|
|
||||||
|
|
||||||
switch (effectiveness) {
|
|
||||||
case 0:
|
|
||||||
damageHpRatio = 0;
|
|
||||||
break;
|
|
||||||
case 0.25:
|
|
||||||
damageHpRatio = 0.03125;
|
|
||||||
break;
|
|
||||||
case 0.5:
|
|
||||||
damageHpRatio = 0.0625;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
damageHpRatio = 0.125;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
damageHpRatio = 0.25;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
damageHpRatio = 0.5;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return damageHpRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
|
||||||
const cancelled = new BooleanHolder(false);
|
|
||||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
|
||||||
if (cancelled.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
|
||||||
if (!damageHpRatio) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (simulated) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("arenaTag:stealthRockActivateTrap", {
|
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
|
||||||
pokemon.turnData.damageTaken += damage;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
|
||||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
|
||||||
return Phaser.Math.Linear(super.getMatchupScoreMultiplier(pokemon), 1, 1 - Math.pow(damageHpRatio, damageHpRatio));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Sticky_Web_(move) Sticky Web}.
|
|
||||||
* Applies up to 1 layer of Sticky Web, which lowers the Speed by one stage
|
|
||||||
* to any Pokémon who is summoned into this trap.
|
|
||||||
*/
|
|
||||||
class StickyWebTag extends ArenaTrapTag {
|
|
||||||
public readonly tagType = ArenaTagType.STICKY_WEB;
|
public readonly tagType = ArenaTagType.STICKY_WEB;
|
||||||
|
public override get maxLayers() {
|
||||||
|
return 1 as const;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||||
super(MoveId.STICKY_WEB, sourceId, side, 1);
|
super(MoveId.STICKY_WEB, sourceId, side);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(arena: Arena, quiet = false): void {
|
protected override getAddMessage(source: Pokemon): string {
|
||||||
super.onAdd(arena);
|
return i18next.t("arenaTag:stickyWebOnAdd", {
|
||||||
|
|
||||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
|
||||||
if (quiet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = this.getSourcePokemon();
|
|
||||||
if (!source) {
|
|
||||||
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("arenaTag:stickyWebOnAdd", {
|
|
||||||
moveName: this.getMoveName(),
|
moveName: this.getMoveName(),
|
||||||
opponentDesc: source.getOpponentDescriptor(),
|
opponentDesc: source.getOpponentDescriptor(),
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||||
if (pokemon.isGrounded()) {
|
|
||||||
const cancelled = new BooleanHolder(false);
|
const cancelled = new BooleanHolder(false);
|
||||||
|
// TODO: Does this need to pass `simulated` as a parameter?
|
||||||
applyAbAttrs("ProtectStatAbAttr", {
|
applyAbAttrs("ProtectStatAbAttr", {
|
||||||
pokemon,
|
pokemon,
|
||||||
cancelled,
|
cancelled,
|
||||||
@ -1062,23 +1051,26 @@ class StickyWebTag extends ArenaTrapTag {
|
|||||||
stages: -1,
|
stages: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (simulated) {
|
if (cancelled.value) {
|
||||||
return !cancelled.value;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simulated) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cancelled.value) {
|
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t("arenaTag:stickyWebActivateTrap", {
|
i18next.t("arenaTag:stickyWebActivateTrap", {
|
||||||
pokemonName: pokemon.getNameToRender(),
|
pokemonName: pokemon.getNameToRender(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const stages = new NumberHolder(-1);
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"StatStageChangePhase",
|
"StatStageChangePhase",
|
||||||
pokemon.getBattlerIndex(),
|
pokemon.getBattlerIndex(),
|
||||||
false,
|
false,
|
||||||
[Stat.SPD],
|
[Stat.SPD],
|
||||||
stages.value,
|
-1,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
@ -1090,7 +1082,73 @@ class StickyWebTag extends ArenaTrapTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
/**
|
||||||
|
* This arena tag facilitates the application of the move Imprison
|
||||||
|
* Imprison remains in effect as long as the source Pokemon is active and present on the field.
|
||||||
|
* Imprison will apply to any opposing Pokemon that switch onto the field as well.
|
||||||
|
*/
|
||||||
|
class ImprisonTag extends EntryHazardTag {
|
||||||
|
public readonly tagType = ArenaTagType.IMPRISON;
|
||||||
|
public override get maxLayers() {
|
||||||
|
return 1 as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||||
|
super(MoveId.IMPRISON, sourceId, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the effects of Imprison to all opposing on-field Pokemon.
|
||||||
|
*/
|
||||||
|
override onAdd(_arena: Arena, quiet = false) {
|
||||||
|
super.onAdd(_arena, quiet);
|
||||||
|
|
||||||
|
const party = this.getAffectedPokemon();
|
||||||
|
party.forEach(p => {
|
||||||
|
if (p.isAllowedInBattle()) {
|
||||||
|
p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override getAddMessage(source: Pokemon): string {
|
||||||
|
return i18next.t("battlerTags:imprisonOnAdd", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the source Pokemon is still active on the field
|
||||||
|
* @param _arena
|
||||||
|
* @returns `true` if the source of the tag is still active on the field | `false` if not
|
||||||
|
*/
|
||||||
|
override lapse(): boolean {
|
||||||
|
const source = this.getSourcePokemon();
|
||||||
|
return !!source?.isActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active
|
||||||
|
* @param {Pokemon} pokemon the Pokemon Imprison is applied to
|
||||||
|
* @returns `true`
|
||||||
|
*/
|
||||||
|
override activateTrap(pokemon: Pokemon): boolean {
|
||||||
|
const source = this.getSourcePokemon();
|
||||||
|
if (source?.isActive(true) && pokemon.isAllowedInBattle()) {
|
||||||
|
pokemon.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon
|
||||||
|
* @param arena
|
||||||
|
*/
|
||||||
|
override onRemove(): void {
|
||||||
|
const party = this.getAffectedPokemon();
|
||||||
|
party.forEach(p => {
|
||||||
|
p.removeTag(BattlerTagType.IMPRISON);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1287,75 +1345,6 @@ class NoneTag extends ArenaTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This arena tag facilitates the application of the move Imprison
|
|
||||||
* Imprison remains in effect as long as the source Pokemon is active and present on the field.
|
|
||||||
* Imprison will apply to any opposing Pokemon that switch onto the field as well.
|
|
||||||
*/
|
|
||||||
class ImprisonTag extends ArenaTrapTag {
|
|
||||||
public readonly tagType = ArenaTagType.IMPRISON;
|
|
||||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
|
||||||
super(MoveId.IMPRISON, sourceId, side, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply the effects of Imprison to all opposing on-field Pokemon.
|
|
||||||
*/
|
|
||||||
override onAdd() {
|
|
||||||
const source = this.getSourcePokemon();
|
|
||||||
if (!source) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const party = this.getAffectedPokemon();
|
|
||||||
party.forEach(p => {
|
|
||||||
if (p.isAllowedInBattle()) {
|
|
||||||
p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("battlerTags:imprisonOnAdd", {
|
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the source Pokemon is still active on the field
|
|
||||||
* @param _arena
|
|
||||||
* @returns `true` if the source of the tag is still active on the field | `false` if not
|
|
||||||
*/
|
|
||||||
override lapse(): boolean {
|
|
||||||
const source = this.getSourcePokemon();
|
|
||||||
return !!source?.isActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active
|
|
||||||
* @param {Pokemon} pokemon the Pokemon Imprison is applied to
|
|
||||||
* @returns `true`
|
|
||||||
*/
|
|
||||||
override activateTrap(pokemon: Pokemon): boolean {
|
|
||||||
const source = this.getSourcePokemon();
|
|
||||||
if (source?.isActive(true) && pokemon.isAllowedInBattle()) {
|
|
||||||
pokemon.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon
|
|
||||||
* @param arena
|
|
||||||
*/
|
|
||||||
override onRemove(): void {
|
|
||||||
const party = this.getAffectedPokemon();
|
|
||||||
party.forEach(p => {
|
|
||||||
p.removeTag(BattlerTagType.IMPRISON);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arena Tag implementing the "sea of fire" effect from the combination
|
* Arena Tag implementing the "sea of fire" effect from the combination
|
||||||
* of {@link https://bulbapedia.bulbagarden.net/wiki/Fire_Pledge_(move) | Fire Pledge}
|
* of {@link https://bulbapedia.bulbagarden.net/wiki/Fire_Pledge_(move) | Fire Pledge}
|
||||||
|
@ -564,7 +564,7 @@ export class BeakBlastChargingTag extends BattlerTag {
|
|||||||
target: pokemon,
|
target: pokemon,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
|
phaseData.attacker.trySetStatus(StatusEffect.BURN, pokemon);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1510,7 +1510,7 @@ export class DrowsyTag extends SerializableBattlerTag {
|
|||||||
|
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
if (!super.lapse(pokemon, lapseType)) {
|
if (!super.lapse(pokemon, lapseType)) {
|
||||||
pokemon.trySetStatus(StatusEffect.SLEEP, true);
|
pokemon.trySetStatus(StatusEffect.SLEEP);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1860,7 +1860,7 @@ export class ContactSetStatusProtectedTag extends DamageProtectedTag {
|
|||||||
* @param user - The pokemon that is being attacked and has the tag
|
* @param user - The pokemon that is being attacked and has the tag
|
||||||
*/
|
*/
|
||||||
override onContact(attacker: Pokemon, user: Pokemon): void {
|
override onContact(attacker: Pokemon, user: Pokemon): void {
|
||||||
attacker.trySetStatus(this.#statusEffect, true, user);
|
attacker.trySetStatus(this.#statusEffect, user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2804,7 +2804,7 @@ export class GulpMissileTag extends SerializableBattlerTag {
|
|||||||
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
||||||
globalScene.phaseManager.unshiftNew("StatStageChangePhase", attacker.getBattlerIndex(), false, [Stat.DEF], -1);
|
globalScene.phaseManager.unshiftNew("StatStageChangePhase", attacker.getBattlerIndex(), false, [Stat.DEF], -1);
|
||||||
} else {
|
} else {
|
||||||
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
|
attacker.trySetStatus(StatusEffect.PARALYSIS, pokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account";
|
|||||||
import type { GameMode } from "#app/game-mode";
|
import type { GameMode } from "#app/game-mode";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import type { ArenaTrapTag } from "#data/arena-tag";
|
import type { EntryHazardTag } from "#data/arena-tag";
|
||||||
import { WeakenMoveTypeTag } from "#data/arena-tag";
|
import { WeakenMoveTypeTag } from "#data/arena-tag";
|
||||||
import { MoveChargeAnim } from "#data/battle-anims";
|
import { MoveChargeAnim } from "#data/battle-anims";
|
||||||
import {
|
import {
|
||||||
@ -88,7 +88,7 @@ import type { AttackMoveResult } from "#types/attack-move-result";
|
|||||||
import type { Localizable } from "#types/locales";
|
import type { Localizable } from "#types/locales";
|
||||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -1190,8 +1190,9 @@ export abstract class MoveAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @virtual
|
* Return this `MoveAttr`'s associated {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}.
|
||||||
* @returns the {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} for this {@linkcode Move}
|
* 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 {
|
getCondition(): MoveCondition | MoveConditionFunc | null {
|
||||||
return null;
|
return null;
|
||||||
@ -1304,15 +1305,21 @@ export class MoveEffectAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the {@linkcode Move}'s effects are valid to {@linkcode apply}
|
* Determine whether this {@linkcode MoveAttr}'s effects are able to {@linkcode apply | be applied} to the target.
|
||||||
* @virtual
|
*
|
||||||
* @param user {@linkcode Pokemon} using the move
|
* Will **NOT** cause the move to fail upon returning `false` (unlike {@linkcode getCondition};
|
||||||
* @param target {@linkcode Pokemon} target of the move
|
* merely that the effect for this attribute will be nullified.
|
||||||
* @param move {@linkcode Move} with this attribute
|
* @param user - The {@linkcode Pokemon} using the move
|
||||||
* @param args Set of unique arguments needed by this attribute
|
* @param target - The {@linkcode Pokemon} being targeted by the move, or {@linkcode user} if the move is
|
||||||
* @returns true if basic application of the ability attribute should be possible
|
* {@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)
|
return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp)
|
||||||
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) ||
|
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) ||
|
||||||
move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }));
|
move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }));
|
||||||
@ -1961,19 +1968,17 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
|||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
*/
|
*/
|
||||||
export class HealAttr extends MoveEffectAttr {
|
export class HealAttr extends MoveEffectAttr {
|
||||||
/** The percentage of {@linkcode Stat.HP} to heal */
|
constructor(
|
||||||
private healRatio: number;
|
/** The percentage of {@linkcode Stat.HP} to heal. */
|
||||||
/** Should an animation be shown? */
|
private healRatio: number,
|
||||||
private showAnim: boolean;
|
/** Whether to display a healing animation when healing the target; default `false` */
|
||||||
|
private showAnim = false,
|
||||||
constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) {
|
selfTarget = true
|
||||||
super(selfTarget === undefined || selfTarget);
|
) {
|
||||||
|
super(selfTarget);
|
||||||
this.healRatio = healRatio || 1;
|
|
||||||
this.showAnim = !!showAnim;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
this.addHealPhase(this.selfTarget ? user : target, this.healRatio);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1982,15 +1987,69 @@ export class HealAttr extends MoveEffectAttr {
|
|||||||
* Creates a new {@linkcode PokemonHealPhase}.
|
* Creates a new {@linkcode PokemonHealPhase}.
|
||||||
* This heals the target and shows the appropriate message.
|
* 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(),
|
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(),
|
||||||
toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim);
|
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;
|
const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10;
|
||||||
return Math.round(score / (1 - this.healRatio / 2));
|
return Math.round(score / (1 - this.healRatio / 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Change to fail move
|
||||||
|
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()) {
|
||||||
|
// Ensure the fail message isn't displayed when checking the move conditions outside of the move execution
|
||||||
|
// TOOD: Fix this in PR#6276
|
||||||
|
if (globalScene.phaseManager.getCurrentPhase()?.is("MovePhase")) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: change after HealAttr is changed to fail move
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2262,20 +2321,9 @@ export class BoostHealAttr extends HealAttr {
|
|||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
*/
|
*/
|
||||||
export class HealOnAllyAttr extends HealAttr {
|
export class HealOnAllyAttr extends HealAttr {
|
||||||
/**
|
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||||
* @param user {@linkcode Pokemon} using the move
|
// Don't trigger if not targeting an ally
|
||||||
* @param target {@linkcode Pokemon} target of the move
|
return target === user.getAlly() && super.canApply(user, target, _move, _args);
|
||||||
* @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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2286,6 +2334,7 @@ export class HealOnAllyAttr extends HealAttr {
|
|||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
* @see {@linkcode getUserBenefitScore}
|
* @see {@linkcode getUserBenefitScore}
|
||||||
*/
|
*/
|
||||||
|
// TODO: Make Strength Sap its own attribute that extends off of this one
|
||||||
export class HitHealAttr extends MoveEffectAttr {
|
export class HitHealAttr extends MoveEffectAttr {
|
||||||
private healRatio: number;
|
private healRatio: number;
|
||||||
private healStat: EffectiveStat | null;
|
private healStat: EffectiveStat | null;
|
||||||
@ -2536,49 +2585,50 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
|
|||||||
|
|
||||||
export class StatusEffectAttr extends MoveEffectAttr {
|
export class StatusEffectAttr extends MoveEffectAttr {
|
||||||
public effect: StatusEffect;
|
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);
|
super(selfTarget);
|
||||||
|
|
||||||
this.effect = effect;
|
this.effect = effect;
|
||||||
this.turnsRemaining = turnsRemaining;
|
|
||||||
this.overrideStatus = overrideStatus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||||
const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance;
|
const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance;
|
||||||
const quiet = move.category !== MoveCategory.STATUS;
|
if (!statusCheck) {
|
||||||
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;
|
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)) {
|
// non-status moves don't play sound effects for failures
|
||||||
|
const quiet = move.category !== MoveCategory.STATUS;
|
||||||
|
|
||||||
|
if (
|
||||||
|
target.trySetStatus(this.effect, user, undefined, null, false, quiet)
|
||||||
|
) {
|
||||||
applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
|
applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
export class MultiStatusEffectAttr extends StatusEffectAttr {
|
||||||
public effects: StatusEffect[];
|
public effects: StatusEffect[];
|
||||||
|
|
||||||
constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
|
constructor(effects: StatusEffect[], selfTarget?: boolean) {
|
||||||
super(effects[0], selfTarget, turnsRemaining, overrideStatus);
|
super(effects[0], selfTarget);
|
||||||
this.effects = effects;
|
this.effects = effects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2611,26 +2661,41 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
|||||||
* @returns - Whether the effect was successfully applied to the target.
|
* @returns - Whether the effect was successfully 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(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
const statusToApply = user.status?.effect ??
|
||||||
|
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||||
|
|
||||||
if (target.status || !statusToApply) {
|
// Bang is justified as condition func returns early if no status is found
|
||||||
|
if (!target.trySetStatus(statusToApply, 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) => {
|
||||||
|
if (target.status?.effect) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||||
|
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||||
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
const statusToApply =
|
||||||
return !target.status && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
user.status?.effect ??
|
||||||
|
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||||
|
|
||||||
|
// TODO: Give this a positive user benefit score
|
||||||
|
return !target.status?.effect && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2690,7 +2755,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
|||||||
* Used for Incinerate and Knock Off.
|
* Used for Incinerate and Knock Off.
|
||||||
* Not Implemented Cases: (Same applies for Thief)
|
* 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 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 {
|
export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||||
|
|
||||||
@ -2900,7 +2965,7 @@ 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);
|
this.effects = coerceArray(effects)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4427,6 +4492,10 @@ export class SpitUpPowerAttr extends VariablePowerAttr {
|
|||||||
* Does NOT remove stockpiled stacks.
|
* Does NOT remove stockpiled stacks.
|
||||||
*/
|
*/
|
||||||
export class SwallowHealAttr extends HealAttr {
|
export class SwallowHealAttr extends HealAttr {
|
||||||
|
constructor() {
|
||||||
|
super(1)
|
||||||
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const stockpilingTag = user.getTag(StockpilingTag);
|
const stockpilingTag = user.getTag(StockpilingTag);
|
||||||
|
|
||||||
@ -6083,7 +6152,7 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr {
|
|||||||
getCondition(): MoveConditionFunc {
|
getCondition(): MoveConditionFunc {
|
||||||
return (user, target, move) => {
|
return (user, target, move) => {
|
||||||
const side = (this.selfSideTarget !== user.isPlayer()) ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER;
|
const side = (this.selfSideTarget !== user.isPlayer()) ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER;
|
||||||
const tag = globalScene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag;
|
const tag = globalScene.arena.getTagOnSide(this.tagType, side) as EntryHazardTag;
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -6107,7 +6176,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
|
|||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||||
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
const tag = globalScene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag;
|
const tag = globalScene.arena.getTagOnSide(this.tagType, side) as EntryHazardTag;
|
||||||
if ((moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
|
if ((moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
|
||||||
globalScene.arena.addTag(this.tagType, 0, move.id, user.id, side);
|
globalScene.arena.addTag(this.tagType, 0, move.id, user.id, side);
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
@ -7909,7 +7978,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
|
|||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
if (target.turnData.statStagesIncreased) {
|
if (target.turnData.statStagesIncreased) {
|
||||||
target.trySetStatus(this.effect, true, user);
|
target.trySetStatus(this.effect, user);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -8056,11 +8125,11 @@ const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
|||||||
return !cancelled.value;
|
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 failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||||
@ -8938,9 +9007,7 @@ export function initMoves() {
|
|||||||
.attr(MultiHitAttr, MultiHitType._2)
|
.attr(MultiHitAttr, MultiHitType._2)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
|
new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
|
||||||
.attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true)
|
.attr(RestAttr, 3)
|
||||||
.attr(HealAttr, 1, true)
|
|
||||||
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user))
|
|
||||||
.triageMove(),
|
.triageMove(),
|
||||||
new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
|
new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
|
||||||
.attr(FlinchAttr)
|
.attr(FlinchAttr)
|
||||||
@ -9286,14 +9353,16 @@ export function initMoves() {
|
|||||||
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
||||||
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
|
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
|
||||||
.condition(hasStockpileStacksCondition)
|
|
||||||
.attr(SpitUpPowerAttr, 100)
|
.attr(SpitUpPowerAttr, 100)
|
||||||
|
.condition(hasStockpileStacksCondition)
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
||||||
new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||||
.condition(hasStockpileStacksCondition)
|
|
||||||
.attr(SwallowHealAttr)
|
.attr(SwallowHealAttr)
|
||||||
|
.condition(hasStockpileStacksCondition)
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true)
|
.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)
|
new AttackMove(MoveId.HEAT_WAVE, PokemonType.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3)
|
||||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||||
.attr(StatusEffectAttr, StatusEffect.BURN)
|
.attr(StatusEffectAttr, StatusEffect.BURN)
|
||||||
@ -9679,14 +9748,8 @@ export function initMoves() {
|
|||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
new StatusMove(MoveId.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
|
new StatusMove(MoveId.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
|
||||||
.attr(PsychoShiftEffectAttr)
|
.attr(PsychoShiftEffectAttr)
|
||||||
.condition((user, target, move) => {
|
// TODO: Verify status applied if a statused pokemon obtains Comatose (via Transform) and uses Psycho Shift
|
||||||
let statusToApply = user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined;
|
.edgeCase(),
|
||||||
if (user.status?.effect && isNonVolatileStatusEffect(user.status.effect)) {
|
|
||||||
statusToApply = user.status.effect;
|
|
||||||
}
|
|
||||||
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
new AttackMove(MoveId.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
|
new AttackMove(MoveId.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
|
||||||
.makesContact()
|
.makesContact()
|
||||||
.attr(LessPPMorePowerAttr),
|
.attr(LessPPMorePowerAttr),
|
||||||
|
@ -243,8 +243,9 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
|
|||||||
if (burnable?.length > 0) {
|
if (burnable?.length > 0) {
|
||||||
const roll = randSeedInt(burnable.length);
|
const roll = randSeedInt(burnable.length);
|
||||||
const chosenPokemon = burnable[roll];
|
const chosenPokemon = burnable[roll];
|
||||||
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
|
if (chosenPokemon.canSetStatus(StatusEffect.BURN, true)) {
|
||||||
// Burn applied
|
// Burn applied
|
||||||
|
chosenPokemon.doSetStatus(StatusEffect.BURN);
|
||||||
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
|
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
|
||||||
encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name);
|
encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name);
|
||||||
queueEncounterMessage(`${namespace}:option.2.targetBurned`);
|
queueEncounterMessage(`${namespace}:option.2.targetBurned`);
|
||||||
|
@ -309,7 +309,7 @@ export function getRandomSpeciesByStarterCost(
|
|||||||
*/
|
*/
|
||||||
export function koPlayerPokemon(pokemon: PlayerPokemon) {
|
export function koPlayerPokemon(pokemon: PlayerPokemon) {
|
||||||
pokemon.hp = 0;
|
pokemon.hp = 0;
|
||||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||||
pokemon.updateInfo();
|
pokemon.updateInfo();
|
||||||
queueEncounterMessage(
|
queueEncounterMessage(
|
||||||
i18next.t("battle:fainted", {
|
i18next.t("battle:fainted", {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/** Enum representing all non-volatile status effects. */
|
||||||
|
// TODO: Remove StatusEffect.FAINT
|
||||||
export enum StatusEffect {
|
export enum StatusEffect {
|
||||||
NONE,
|
NONE,
|
||||||
POISON,
|
POISON,
|
||||||
|
@ -8,7 +8,7 @@ import Overrides from "#app/overrides";
|
|||||||
import type { BiomeTierTrainerPools, PokemonPools } from "#balance/biomes";
|
import type { BiomeTierTrainerPools, PokemonPools } from "#balance/biomes";
|
||||||
import { BiomePoolTier, biomePokemonPools, biomeTrainerPools } from "#balance/biomes";
|
import { BiomePoolTier, biomePokemonPools, biomeTrainerPools } from "#balance/biomes";
|
||||||
import type { ArenaTag } from "#data/arena-tag";
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
import { ArenaTrapTag, getArenaTag } from "#data/arena-tag";
|
import { EntryHazardTag, getArenaTag } from "#data/arena-tag";
|
||||||
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { PositionalTagManager } from "#data/positional-tags/positional-tag-manager";
|
import { PositionalTagManager } from "#data/positional-tags/positional-tag-manager";
|
||||||
@ -709,8 +709,8 @@ export class Arena {
|
|||||||
if (existingTag) {
|
if (existingTag) {
|
||||||
existingTag.onOverlap(this, globalScene.getPokemonById(sourceId));
|
existingTag.onOverlap(this, globalScene.getPokemonById(sourceId));
|
||||||
|
|
||||||
if (existingTag instanceof ArenaTrapTag) {
|
if (existingTag instanceof EntryHazardTag) {
|
||||||
const { tagType, side, turnCount, layers, maxLayers } = existingTag as ArenaTrapTag;
|
const { tagType, side, turnCount, layers, maxLayers } = existingTag as EntryHazardTag;
|
||||||
this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers));
|
this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -723,7 +723,7 @@ export class Arena {
|
|||||||
newTag.onAdd(this, quiet);
|
newTag.onAdd(this, quiet);
|
||||||
this.tags.push(newTag);
|
this.tags.push(newTag);
|
||||||
|
|
||||||
const { layers = 0, maxLayers = 0 } = newTag instanceof ArenaTrapTag ? newTag : {};
|
const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {};
|
||||||
|
|
||||||
this.eventTarget.dispatchEvent(
|
this.eventTarget.dispatchEvent(
|
||||||
new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, layers, maxLayers),
|
new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, layers, maxLayers),
|
||||||
|
@ -237,6 +237,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
public ivs: number[];
|
public ivs: number[];
|
||||||
public nature: Nature;
|
public nature: Nature;
|
||||||
public moveset: PokemonMove[];
|
public moveset: PokemonMove[];
|
||||||
|
/**
|
||||||
|
* This Pokemon's current {@link https://m.bulbapedia.bulbagarden.net/wiki/Status_condition#Non-volatile_status | non-volatile status condition},
|
||||||
|
* or `null` if none exist.
|
||||||
|
* @todo Make private
|
||||||
|
*/
|
||||||
public status: Status | null;
|
public status: Status | null;
|
||||||
public friendship: number;
|
public friendship: number;
|
||||||
public metLevel: number;
|
public metLevel: number;
|
||||||
@ -4746,7 +4751,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param reason - The reason for the status application failure -
|
* @param reason - The reason for the status application failure -
|
||||||
* can be "overlap" (already has same status), "other" (generic fail message)
|
* can be "overlap" (already has same status), "other" (generic fail message)
|
||||||
* or a {@linkcode TerrainType} for terrain-based blockages.
|
* or a {@linkcode TerrainType} for terrain-based blockages.
|
||||||
* Defaults to "other".
|
* Default `"other"`
|
||||||
*/
|
*/
|
||||||
queueStatusImmuneMessage(
|
queueStatusImmuneMessage(
|
||||||
quiet: boolean,
|
quiet: boolean,
|
||||||
@ -4775,15 +4780,20 @@ export 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 effect - The {@linkcode StatusEffect} whose applicability is being checked
|
||||||
* @param quiet Whether in-battle messages should trigger or not
|
* @param quiet - Whether to suppress in-battle messages for status checks; default `false`
|
||||||
* @param overrideStatus Whether the Pokemon's current status can be overriden
|
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`
|
||||||
* @param sourcePokemon The Pokemon that is setting the status effect
|
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||||
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
|
* 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.
|
||||||
*/
|
*/
|
||||||
canSetStatus(
|
// 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`
|
||||||
|
public canSetStatus(
|
||||||
effect: StatusEffect,
|
effect: StatusEffect,
|
||||||
quiet = false,
|
quiet = false,
|
||||||
overrideStatus = false,
|
overrideStatus = false,
|
||||||
@ -4791,6 +4801,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
ignoreField = false,
|
ignoreField = false,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (effect !== StatusEffect.FAINT) {
|
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) {
|
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
||||||
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
|
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
|
||||||
return false;
|
return false;
|
||||||
@ -4803,73 +4815,62 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
const types = this.getTypes(true, true);
|
const types = this.getTypes(true, true);
|
||||||
|
|
||||||
|
/* Whether the target is immune to the specific status being applied. */
|
||||||
|
let isImmune = false;
|
||||||
|
/** The reason for a potential blockage; default "other" for type-based. */
|
||||||
|
let reason: "other" | Exclude<TerrainType, TerrainType.NONE> = "other";
|
||||||
|
|
||||||
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 from the applier.
|
||||||
const poisonImmunity = types.map(defType => {
|
isImmune = types.some(defType => {
|
||||||
// Check if the Pokemon is not immune to Poison/Toxic
|
// only 1 immunity needed to block
|
||||||
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
|
// No source (such as from Toxic Spikes) = blocked by default
|
||||||
|
if (!sourcePokemon) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const cancelImmunity = new BooleanHolder(false);
|
const cancelImmunity = new BooleanHolder(false);
|
||||||
// TODO: Determine if we need to pass `quiet` as the value for simulated in this call
|
// TODO: Determine if we need to pass `quiet` as the value for simulated in this call
|
||||||
if (sourcePokemon) {
|
|
||||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
||||||
pokemon: sourcePokemon,
|
pokemon: sourcePokemon,
|
||||||
cancelled: cancelImmunity,
|
cancelled: cancelImmunity,
|
||||||
statusEffect: effect,
|
statusEffect: effect,
|
||||||
defenderType: defType,
|
defenderType: 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.queueStatusImmuneMessage(quiet);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case StatusEffect.PARALYSIS:
|
case StatusEffect.PARALYSIS:
|
||||||
if (this.isOfType(PokemonType.ELECTRIC)) {
|
isImmune = this.isOfType(PokemonType.ELECTRIC);
|
||||||
this.queueStatusImmuneMessage(quiet);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case StatusEffect.SLEEP:
|
case StatusEffect.SLEEP:
|
||||||
if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) {
|
isImmune = this.isGrounded() && globalScene.arena.getTerrainType() === TerrainType.ELECTRIC;
|
||||||
this.queueStatusImmuneMessage(quiet, TerrainType.ELECTRIC);
|
reason = TerrainType.ELECTRIC;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case StatusEffect.FREEZE:
|
case StatusEffect.FREEZE: {
|
||||||
if (
|
const weatherType = globalScene.arena.getWeatherType();
|
||||||
|
isImmune =
|
||||||
this.isOfType(PokemonType.ICE) ||
|
this.isOfType(PokemonType.ICE) ||
|
||||||
(!ignoreField &&
|
(!ignoreField && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN));
|
||||||
globalScene?.arena?.weather?.weatherType &&
|
|
||||||
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType))
|
|
||||||
) {
|
|
||||||
this.queueStatusImmuneMessage(quiet);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case StatusEffect.BURN:
|
|
||||||
if (this.isOfType(PokemonType.FIRE)) {
|
|
||||||
this.queueStatusImmuneMessage(quiet);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
case StatusEffect.BURN:
|
||||||
|
isImmune = this.isOfType(PokemonType.FIRE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isImmune) {
|
||||||
|
this.queueStatusImmuneMessage(quiet, reason);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cancellations from self/ally abilities
|
||||||
const cancelled = new BooleanHolder(false);
|
const cancelled = new BooleanHolder(false);
|
||||||
applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet });
|
applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet });
|
||||||
if (cancelled.value) {
|
if (cancelled.value) {
|
||||||
@ -4885,15 +4886,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
target: this,
|
target: this,
|
||||||
source: sourcePokemon,
|
source: sourcePokemon,
|
||||||
});
|
});
|
||||||
if (cancelled.value) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled.value) {
|
if (cancelled.value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform safeguard checks
|
||||||
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
|
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
@ -4906,18 +4904,36 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
trySetStatus(
|
/**
|
||||||
effect?: StatusEffect,
|
* Attempt to set this Pokemon's status to the specified condition.
|
||||||
asPhase = false,
|
* 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 - String containing text to be displayed upon status setting; defaults to normal key for status
|
||||||
|
* and is used exclusively for Rest
|
||||||
|
* @returns Whether the status effect phase was successfully created.
|
||||||
|
* @see {@linkcode doSetStatus} - alternate function that sets status immediately (albeit without condition checks).
|
||||||
|
*/
|
||||||
|
public trySetStatus(
|
||||||
|
effect: StatusEffect,
|
||||||
sourcePokemon: Pokemon | null = null,
|
sourcePokemon: Pokemon | null = null,
|
||||||
turnsRemaining = 0,
|
sleepTurnsRemaining?: number,
|
||||||
sourceText: string | null = null,
|
sourceText: string | null = null,
|
||||||
overrideStatus?: boolean,
|
overrideStatus?: boolean,
|
||||||
quiet = true,
|
quiet = true,
|
||||||
|
overrideMessage?: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
// TODO: This needs to propagate failure status for status moves
|
||||||
if (!effect) {
|
if (!effect) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
|
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -4937,48 +4953,79 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asPhase) {
|
|
||||||
if (overrideStatus) {
|
if (overrideStatus) {
|
||||||
this.resetStatus(false);
|
this.resetStatus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"ObtainStatusEffectPhase",
|
"ObtainStatusEffectPhase",
|
||||||
this.getBattlerIndex(),
|
this.getBattlerIndex(),
|
||||||
effect,
|
effect,
|
||||||
turnsRemaining,
|
|
||||||
sourceText,
|
|
||||||
sourcePokemon,
|
sourcePokemon,
|
||||||
|
sleepTurnsRemaining,
|
||||||
|
sourceText,
|
||||||
|
overrideMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sleepTurnsRemaining: NumberHolder;
|
/**
|
||||||
|
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||||
|
* @param effect - The {@linkcode StatusEffect} to set
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||||
|
*/
|
||||||
|
doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void;
|
||||||
|
/**
|
||||||
|
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||||
|
* @param effect - {@linkcode StatusEffect.SLEEP}
|
||||||
|
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||||
|
*/
|
||||||
|
doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void;
|
||||||
|
/**
|
||||||
|
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||||
|
* @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
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||||
|
*/
|
||||||
|
doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void;
|
||||||
|
/**
|
||||||
|
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||||
|
* @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
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||||
|
* @todo Make this and all related fields private and change tests to use a field-based helper or similar
|
||||||
|
*/
|
||||||
|
doSetStatus(
|
||||||
|
effect: StatusEffect,
|
||||||
|
sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : 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 semi-invulnerable when put asleep (such as due to Yawm),
|
||||||
const invulnerableTags = [
|
// remove their invulnerability and cancel the upcoming move from the queue
|
||||||
|
const invulnTagTypes = [
|
||||||
|
BattlerTagType.FLYING,
|
||||||
BattlerTagType.UNDERGROUND,
|
BattlerTagType.UNDERGROUND,
|
||||||
BattlerTagType.UNDERWATER,
|
BattlerTagType.UNDERWATER,
|
||||||
BattlerTagType.HIDDEN,
|
BattlerTagType.HIDDEN,
|
||||||
BattlerTagType.FLYING,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const tag = invulnerableTags.find(t => this.getTag(t));
|
if (this.findTag(t => invulnTagTypes.includes(t.tagType))) {
|
||||||
|
this.findAndRemoveTags(t => invulnTagTypes.includes(t.tagType));
|
||||||
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);
|
||||||
this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||||
import { signatureSpecies } from "#balance/signature-species";
|
import { signatureSpecies } from "#balance/signature-species";
|
||||||
import { ArenaTrapTag } from "#data/arena-tag";
|
import { EntryHazardTag } from "#data/arena-tag";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||||
@ -584,8 +584,8 @@ export class Trainer extends Phaser.GameObjects.Container {
|
|||||||
score /= playerField.length;
|
score /= playerField.length;
|
||||||
if (forSwitch && !p.isOnField()) {
|
if (forSwitch && !p.isOnField()) {
|
||||||
globalScene.arena
|
globalScene.arena
|
||||||
.findTagsOnSide(t => t instanceof ArenaTrapTag, ArenaTagSide.ENEMY)
|
.findTagsOnSide(t => t instanceof EntryHazardTag, ArenaTagSide.ENEMY)
|
||||||
.map(t => (score *= (t as ArenaTrapTag).getMatchupScoreMultiplier(p)));
|
.map(t => (score *= (t as EntryHazardTag).getMatchupScoreMultiplier(p)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1733,12 +1733,12 @@ export class TurnStatusEffectModifier extends PokemonHeldItemModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to inflicts the holder with the associated {@linkcode StatusEffect}.
|
* Attempt to inflict the holder with the associated {@linkcode StatusEffect}.
|
||||||
* @param pokemon {@linkcode Pokemon} that holds the held item
|
* @param pokemon - The {@linkcode Pokemon} holding 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, undefined, undefined, this.type.name);
|
return pokemon.trySetStatus(this.effect, pokemon, undefined, this.type.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMaxHeldItemCount(_pokemon: Pokemon): number {
|
getMaxHeldItemCount(_pokemon: Pokemon): number {
|
||||||
@ -3605,7 +3605,7 @@ export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifi
|
|||||||
*/
|
*/
|
||||||
override apply(enemyPokemon: Pokemon): boolean {
|
override apply(enemyPokemon: Pokemon): boolean {
|
||||||
if (randSeedFloat() <= this.chance * this.getStackCount()) {
|
if (randSeedFloat() <= this.chance * this.getStackCount()) {
|
||||||
return enemyPokemon.trySetStatus(this.effect, true);
|
return enemyPokemon.trySetStatus(this.effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -271,7 +271,7 @@ export class AttemptCapturePhase extends PokemonPhase {
|
|||||||
const removePokemon = () => {
|
const removePokemon = () => {
|
||||||
globalScene.addFaintedEnemyScore(pokemon);
|
globalScene.addFaintedEnemyScore(pokemon);
|
||||||
pokemon.hp = 0;
|
pokemon.hp = 0;
|
||||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||||
globalScene.clearEnemyHeldItemModifiers();
|
globalScene.clearEnemyHeldItemModifiers();
|
||||||
pokemon.leaveField(true, true, true);
|
pokemon.leaveField(true, true, true);
|
||||||
};
|
};
|
||||||
|
@ -45,7 +45,7 @@ export class AttemptRunPhase extends FieldPhase {
|
|||||||
enemyField.forEach(enemyPokemon => {
|
enemyField.forEach(enemyPokemon => {
|
||||||
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
|
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
|
||||||
enemyPokemon.hp = 0;
|
enemyPokemon.hp = 0;
|
||||||
enemyPokemon.trySetStatus(StatusEffect.FAINT);
|
enemyPokemon.doSetStatus(StatusEffect.FAINT);
|
||||||
});
|
});
|
||||||
|
|
||||||
globalScene.phaseManager.pushNew("BattleEndPhase", false);
|
globalScene.phaseManager.pushNew("BattleEndPhase", false);
|
||||||
|
@ -205,7 +205,7 @@ export class FaintPhase extends PokemonPhase {
|
|||||||
pokemon.lapseTags(BattlerTagLapseType.FAINT);
|
pokemon.lapseTags(BattlerTagLapseType.FAINT);
|
||||||
|
|
||||||
pokemon.y -= 150;
|
pokemon.y -= 150;
|
||||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||||
if (pokemon.isPlayer()) {
|
if (pokemon.isPlayer()) {
|
||||||
globalScene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon);
|
globalScene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon);
|
||||||
} else {
|
} else {
|
||||||
|
@ -269,8 +269,8 @@ export class MovePhase extends BattlePhase {
|
|||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||||
);
|
);
|
||||||
this.pokemon.resetStatus();
|
// cannot use `asPhase=true` as it will cause status to be reset _after_ move condition checks fire
|
||||||
this.pokemon.updateInfo();
|
this.pokemon.resetStatus(false, false, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,55 +3,56 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { CommonBattleAnim } from "#data/battle-anims";
|
import { CommonBattleAnim } from "#data/battle-anims";
|
||||||
import { SpeciesFormChangeStatusEffectTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangeStatusEffectTrigger } from "#data/form-change-triggers";
|
||||||
import { getStatusEffectObtainText, getStatusEffectOverlapText } from "#data/status-effect";
|
import { getStatusEffectObtainText } from "#data/status-effect";
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
import { CommonAnim } from "#enums/move-anims-common";
|
import { CommonAnim } from "#enums/move-anims-common";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
export class ObtainStatusEffectPhase extends PokemonPhase {
|
export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||||
public readonly phaseName = "ObtainStatusEffectPhase";
|
public readonly phaseName = "ObtainStatusEffectPhase";
|
||||||
private statusEffect?: StatusEffect;
|
|
||||||
private turnsRemaining?: number;
|
|
||||||
private sourceText?: string | null;
|
|
||||||
private sourcePokemon?: Pokemon | null;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 statusMessage - A string containing text to be displayed upon status setting;
|
||||||
|
* defaults to normal key for status if empty or omitted.
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
battlerIndex: BattlerIndex,
|
battlerIndex: BattlerIndex,
|
||||||
statusEffect?: StatusEffect,
|
private statusEffect: StatusEffect,
|
||||||
turnsRemaining?: number,
|
private sourcePokemon: Pokemon | null = null,
|
||||||
sourceText?: string | null,
|
private sleepTurnsRemaining?: number,
|
||||||
sourcePokemon?: Pokemon | null,
|
sourceText: string | null = null, // TODO: This should take `undefined` instead of `null`
|
||||||
|
private statusMessage = "",
|
||||||
) {
|
) {
|
||||||
super(battlerIndex);
|
super(battlerIndex);
|
||||||
|
|
||||||
this.statusEffect = statusEffect;
|
this.statusMessage ||= getStatusEffectObtainText(
|
||||||
this.turnsRemaining = turnsRemaining;
|
statusEffect,
|
||||||
this.sourceText = sourceText;
|
getPokemonNameWithAffix(this.getPokemon()),
|
||||||
this.sourcePokemon = sourcePokemon;
|
sourceText ?? undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const pokemon = this.getPokemon();
|
const pokemon = this.getPokemon();
|
||||||
if (pokemon && !pokemon.status) {
|
|
||||||
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
|
pokemon.doSetStatus(this.statusEffect, this.sleepTurnsRemaining);
|
||||||
if (this.turnsRemaining) {
|
|
||||||
pokemon.status!.sleepTurnsRemaining = this.turnsRemaining;
|
|
||||||
}
|
|
||||||
pokemon.updateInfo(true);
|
pokemon.updateInfo(true);
|
||||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => {
|
|
||||||
globalScene.phaseManager.queueMessage(
|
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(false, () => {
|
||||||
getStatusEffectObtainText(
|
globalScene.phaseManager.queueMessage(this.statusMessage);
|
||||||
this.statusEffect,
|
if (this.statusEffect && this.statusEffect !== StatusEffect.FAINT) {
|
||||||
getPokemonNameWithAffix(pokemon),
|
|
||||||
this.sourceText ?? undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
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
|
// 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);
|
globalScene.arena.setIgnoreAbilities(false);
|
||||||
applyAbAttrs("PostSetStatusAbAttr", {
|
applyAbAttrs("PostSetStatusAbAttr", {
|
||||||
pokemon,
|
pokemon,
|
||||||
@ -61,13 +62,5 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
this.end();
|
this.end();
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (pokemon.status?.effect === this.statusEffect) {
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.end();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { HealAchv } from "#system/achv";
|
|||||||
import { NumberHolder } from "#utils/common";
|
import { NumberHolder } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
// TODO: Refactor this - it has far too many arguments
|
||||||
export class PokemonHealPhase extends CommonAnimPhase {
|
export class PokemonHealPhase extends CommonAnimPhase {
|
||||||
public readonly phaseName = "PokemonHealPhase";
|
public readonly phaseName = "PokemonHealPhase";
|
||||||
private hpHealed: number;
|
private hpHealed: number;
|
||||||
@ -28,7 +29,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
|||||||
battlerIndex: BattlerIndex,
|
battlerIndex: BattlerIndex,
|
||||||
hpHealed: number,
|
hpHealed: number,
|
||||||
message: string | null,
|
message: string | null,
|
||||||
showFullHpMessage: boolean,
|
showFullHpMessage = true,
|
||||||
skipAnim = false,
|
skipAnim = false,
|
||||||
revive = false,
|
revive = false,
|
||||||
healStatus = false,
|
healStatus = false,
|
||||||
@ -72,6 +73,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
|||||||
this.message = null;
|
this.message = null;
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (healOrDamage) {
|
if (healOrDamage) {
|
||||||
const hpRestoreMultiplier = new NumberHolder(1);
|
const hpRestoreMultiplier = new NumberHolder(1);
|
||||||
if (!this.revive) {
|
if (!this.revive) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { ArenaTrapTag } from "#data/arena-tag";
|
import { EntryHazardTag } from "#data/arena-tag";
|
||||||
import { MysteryEncounterPostSummonTag } from "#data/battler-tags";
|
import { MysteryEncounterPostSummonTag } from "#data/battler-tags";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
@ -16,7 +16,7 @@ export class PostSummonPhase extends PokemonPhase {
|
|||||||
if (pokemon.status?.effect === StatusEffect.TOXIC) {
|
if (pokemon.status?.effect === StatusEffect.TOXIC) {
|
||||||
pokemon.status.toxicTurnCount = 0;
|
pokemon.status.toxicTurnCount = 0;
|
||||||
}
|
}
|
||||||
globalScene.arena.applyTags(ArenaTrapTag, false, pokemon);
|
globalScene.arena.applyTags(EntryHazardTag, false, pokemon);
|
||||||
|
|
||||||
// If this is mystery encounter and has post summon phase tag, apply post summon effects
|
// If this is mystery encounter and has post summon phase tag, apply post summon effects
|
||||||
if (
|
if (
|
||||||
|
@ -10,7 +10,7 @@ import { Tutorial } from "#app/tutorial";
|
|||||||
import { speciesEggMoves } from "#balance/egg-moves";
|
import { speciesEggMoves } from "#balance/egg-moves";
|
||||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||||
import { speciesStarterCosts } from "#balance/starters";
|
import { speciesStarterCosts } from "#balance/starters";
|
||||||
import { ArenaTrapTag } from "#data/arena-tag";
|
import { EntryHazardTag } from "#data/arena-tag";
|
||||||
import { allMoves, allSpecies } from "#data/data-lists";
|
import { allMoves, allSpecies } from "#data/data-lists";
|
||||||
import type { Egg } from "#data/egg";
|
import type { Egg } from "#data/egg";
|
||||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||||
@ -1135,8 +1135,8 @@ export class GameData {
|
|||||||
globalScene.arena.tags = sessionData.arena.tags;
|
globalScene.arena.tags = sessionData.arena.tags;
|
||||||
if (globalScene.arena.tags) {
|
if (globalScene.arena.tags) {
|
||||||
for (const tag of globalScene.arena.tags) {
|
for (const tag of globalScene.arena.tags) {
|
||||||
if (tag instanceof ArenaTrapTag) {
|
if (tag instanceof EntryHazardTag) {
|
||||||
const { tagType, side, turnCount, layers, maxLayers } = tag as ArenaTrapTag;
|
const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag;
|
||||||
globalScene.arena.eventTarget.dispatchEvent(
|
globalScene.arena.eventTarget.dispatchEvent(
|
||||||
new TagAddedEvent(tagType, side, turnCount, layers, maxLayers),
|
new TagAddedEvent(tagType, side, turnCount, layers, maxLayers),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { ArenaTrapTag } from "#data/arena-tag";
|
import { EntryHazardTag } from "#data/arena-tag";
|
||||||
import { TerrainType } from "#data/terrain";
|
import { TerrainType } from "#data/terrain";
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
@ -287,7 +287,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
|||||||
switch (arenaEffectChangedEvent.constructor) {
|
switch (arenaEffectChangedEvent.constructor) {
|
||||||
case TagAddedEvent: {
|
case TagAddedEvent: {
|
||||||
const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent;
|
const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent;
|
||||||
const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof ArenaTrapTag;
|
const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof EntryHazardTag;
|
||||||
let arenaEffectType: ArenaEffectType;
|
let arenaEffectType: ArenaEffectType;
|
||||||
|
|
||||||
if (tagAddedEvent.arenaTagSide === ArenaTagSide.BOTH) {
|
if (tagAddedEvent.arenaTagSide === ArenaTagSide.BOTH) {
|
||||||
|
2
test/@types/vitest.d.ts
vendored
2
test/@types/vitest.d.ts
vendored
@ -1,3 +1,5 @@
|
|||||||
|
import "vitest";
|
||||||
|
|
||||||
import type { TerrainType } from "#app/data/terrain";
|
import type { TerrainType } from "#app/data/terrain";
|
||||||
import type Overrides from "#app/overrides";
|
import type Overrides from "#app/overrides";
|
||||||
import type { ArenaTag } from "#data/arena-tag";
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
@ -22,25 +23,66 @@ describe("Abilities - Corrosion", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.GRIMER)
|
.enemySpecies(SpeciesId.GRIMER)
|
||||||
.enemyAbility(AbilityId.CORROSION)
|
.ability(AbilityId.CORROSION)
|
||||||
.enemyMoveset(MoveId.TOXIC);
|
.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 () => {
|
it.each<{ name: string; species: SpeciesId }>([
|
||||||
game.override.ability(AbilityId.SYNCHRONIZE);
|
{ name: "Poison", species: SpeciesId.GRIMER },
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
{ 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 enemy = game.field.getEnemyPokemon();
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
expect(enemy.status?.effect).toBeUndefined();
|
||||||
expect(playerPokemon!.status).toBeUndefined();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
game.move.use(MoveId.POISON_GAS);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
await game.toEndOfTurn();
|
||||||
expect(playerPokemon!.status).toBeDefined();
|
|
||||||
expect(enemyPokemon!.status).toBeUndefined();
|
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.use(MoveId.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(salazzle.status?.effect).toBe(StatusEffect.TOXIC);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,6 +49,7 @@ describe("Abilities - Healer", () => {
|
|||||||
const user = game.field.getPlayerPokemon();
|
const user = game.field.getPlayerPokemon();
|
||||||
// Only want one magikarp to have the ability
|
// Only want one magikarp to have the ability
|
||||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
game.move.select(MoveId.SPLASH);
|
||||||
// faint the ally
|
// faint the ally
|
||||||
game.move.select(MoveId.LUNAR_DANCE, 1);
|
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 () => {
|
it("should heal the status of an ally if the ally has a status", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||||
const [user, ally] = game.scene.getPlayerField();
|
const [user, ally] = game.scene.getPlayerField();
|
||||||
|
|
||||||
// Only want one magikarp to have the ability.
|
// Only want one magikarp to have the ability.
|
||||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
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);
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
game.move.select(MoveId.SPLASH, 1);
|
||||||
|
|
||||||
@ -80,7 +82,7 @@ describe("Abilities - Healer", () => {
|
|||||||
const [user, ally] = game.scene.getPlayerField();
|
const [user, ally] = game.scene.getPlayerField();
|
||||||
// Only want one magikarp to have the ability.
|
// Only want one magikarp to have the ability.
|
||||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
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);
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
game.move.select(MoveId.SPLASH, 1);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
@ -79,9 +79,9 @@ describe("Abilities - Infiltrator", () => {
|
|||||||
|
|
||||||
game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, MoveId.NONE, enemy.id, ArenaTagSide.ENEMY, true);
|
game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, MoveId.NONE, enemy.id, ArenaTagSide.ENEMY, true);
|
||||||
|
|
||||||
game.move.select(MoveId.SPORE);
|
game.move.use(MoveId.SPORE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
|
||||||
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
expect(player.waveData.abilitiesApplied).toContain(AbilityId.INFILTRATOR);
|
expect(player.waveData.abilitiesApplied).toContain(AbilityId.INFILTRATOR);
|
||||||
});
|
});
|
||||||
|
@ -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/test-utils/game-manager";
|
|
||||||
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/test-utils/game-manager";
|
|
||||||
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/test-utils/game-manager";
|
|
||||||
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 "#data/data-lists";
|
||||||
|
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 { StatusEffectAttr } from "#moves/move";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
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 = toTitleCase(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);
|
||||||
|
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/test-utils/game-manager";
|
|
||||||
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/test-utils/game-manager";
|
|
||||||
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/test-utils/game-manager";
|
|
||||||
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/test-utils/game-manager";
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -6,7 +6,7 @@ import { GameManager } from "#test/test-utils/game-manager";
|
|||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("Abilities - Immunity", () => {
|
describe("Spec - Pokemon Functions", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
let game: GameManager;
|
let game: GameManager;
|
||||||
|
|
||||||
@ -23,29 +23,29 @@ describe("Abilities - Immunity", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
|
.startingLevel(100)
|
||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.ability(AbilityId.BALL_FETCH)
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove poison when gained", async () => {
|
describe("doSetStatus", () => {
|
||||||
game.override
|
it("should change the Pokemon's status, ignoring feasibility checks", async () => {
|
||||||
.ability(AbilityId.IMMUNITY)
|
await game.classicMode.startBattle([SpeciesId.ACCELGOR]);
|
||||||
.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);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
const player = game.field.getPlayerPokemon();
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
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);
|
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.field.getPlayerPokemon();
|
|
||||||
expect(pkm).toBeDefined();
|
|
||||||
|
|
||||||
expect(pkm.trySetStatus(undefined)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Add To Party", () => {
|
describe("Add To Party", () => {
|
||||||
let scene: BattleScene;
|
let scene: BattleScene;
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ describe("Moves - Beat Up", () => {
|
|||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
game.scene.getPlayerParty()[1].trySetStatus(StatusEffect.BURN);
|
game.scene.getPlayerParty()[1].doSetStatus(StatusEffect.BURN);
|
||||||
|
|
||||||
game.move.select(MoveId.BEAT_UP);
|
game.move.select(MoveId.BEAT_UP);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ArenaTrapTag } from "#data/arena-tag";
|
import { EntryHazardTag } from "#data/arena-tag";
|
||||||
import { allMoves } from "#data/data-lists";
|
import { allMoves } from "#data/data-lists";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
@ -50,12 +50,12 @@ describe("Moves - Ceaseless Edge", () => {
|
|||||||
|
|
||||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
// Spikes should not have any layers before move effect is applied
|
// Spikes should not have any layers before move effect is applied
|
||||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||||
expect(tagBefore instanceof ArenaTrapTag).toBeFalsy();
|
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||||
expect(tagAfter instanceof ArenaTrapTag).toBeTruthy();
|
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||||
expect(tagAfter.layers).toBe(1);
|
expect(tagAfter.layers).toBe(1);
|
||||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||||
});
|
});
|
||||||
@ -72,12 +72,12 @@ describe("Moves - Ceaseless Edge", () => {
|
|||||||
|
|
||||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
// Spikes should not have any layers before move effect is applied
|
// Spikes should not have any layers before move effect is applied
|
||||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||||
expect(tagBefore instanceof ArenaTrapTag).toBeFalsy();
|
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||||
expect(tagAfter instanceof ArenaTrapTag).toBeTruthy();
|
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||||
expect(tagAfter.layers).toBe(2);
|
expect(tagAfter.layers).toBe(2);
|
||||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||||
});
|
});
|
||||||
@ -90,12 +90,12 @@ describe("Moves - Ceaseless Edge", () => {
|
|||||||
game.move.select(MoveId.CEASELESS_EDGE);
|
game.move.select(MoveId.CEASELESS_EDGE);
|
||||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
// Spikes should not have any layers before move effect is applied
|
// Spikes should not have any layers before move effect is applied
|
||||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||||
expect(tagBefore instanceof ArenaTrapTag).toBeFalsy();
|
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||||
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||||
expect(tagAfter instanceof ArenaTrapTag).toBeTruthy();
|
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||||
expect(tagAfter.layers).toBe(2);
|
expect(tagAfter.layers).toBe(2);
|
||||||
|
|
||||||
const hpBeforeSpikes = game.scene.currentBattle.enemyParty[1].hp;
|
const hpBeforeSpikes = game.scene.currentBattle.enemyParty[1].hp;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ArenaTrapTag } from "#data/arena-tag";
|
import type { EntryHazardTag } from "#data/arena-tag";
|
||||||
import { allMoves } from "#data/data-lists";
|
import { allMoves } from "#data/data-lists";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
@ -195,7 +195,7 @@ describe("Moves - Destiny Bond", () => {
|
|||||||
expect(playerPokemon.isFainted()).toBe(true);
|
expect(playerPokemon.isFainted()).toBe(true);
|
||||||
|
|
||||||
// Ceaseless Edge spikes effect should still activate
|
// Ceaseless Edge spikes effect should still activate
|
||||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||||
expect(tagAfter.tagType).toBe(ArenaTagType.SPIKES);
|
expect(tagAfter.tagType).toBe(ArenaTagType.SPIKES);
|
||||||
expect(tagAfter.layers).toBe(1);
|
expect(tagAfter.layers).toBe(1);
|
||||||
});
|
});
|
||||||
@ -220,7 +220,10 @@ describe("Moves - Destiny Bond", () => {
|
|||||||
expect(playerPokemon1?.isFainted()).toBe(true);
|
expect(playerPokemon1?.isFainted()).toBe(true);
|
||||||
|
|
||||||
// Pledge secondary effect should still activate
|
// Pledge secondary effect should still activate
|
||||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.GRASS_WATER_PLEDGE, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
const tagAfter = game.scene.arena.getTagOnSide(
|
||||||
|
ArenaTagType.GRASS_WATER_PLEDGE,
|
||||||
|
ArenaTagSide.ENEMY,
|
||||||
|
) as EntryHazardTag;
|
||||||
expect(tagAfter.tagType).toBe(ArenaTagType.GRASS_WATER_PLEDGE);
|
expect(tagAfter.tagType).toBe(ArenaTagType.GRASS_WATER_PLEDGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
233
test/moves/entry-hazards.test.ts
Normal file
233
test/moves/entry-hazards.test.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import type { TypeDamageMultiplier } from "#data/type";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { BattleType } from "#enums/battle-type";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import type { EntryHazardTagType } from "#types/arena-tags";
|
||||||
|
import i18next from "i18next";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Entry Hazards", () => {
|
||||||
|
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")
|
||||||
|
.enemySpecies(SpeciesId.BLISSEY)
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(100)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.ability(AbilityId.BALL_FETCH)
|
||||||
|
.enemyMoveset(MoveId.SPLASH)
|
||||||
|
.battleType(BattleType.TRAINER);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each<{ name: string; move: MoveId; tagType: EntryHazardTagType }>([
|
||||||
|
{ name: "Spikes", move: MoveId.SPIKES, tagType: ArenaTagType.SPIKES },
|
||||||
|
{
|
||||||
|
name: "Toxic Spikes",
|
||||||
|
move: MoveId.TOXIC_SPIKES,
|
||||||
|
tagType: ArenaTagType.TOXIC_SPIKES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Stealth Rock",
|
||||||
|
move: MoveId.STEALTH_ROCK,
|
||||||
|
tagType: ArenaTagType.STEALTH_ROCK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sticky Web",
|
||||||
|
move: MoveId.STICKY_WEB,
|
||||||
|
tagType: ArenaTagType.STICKY_WEB,
|
||||||
|
},
|
||||||
|
])("General checks - $name", ({ move, tagType }) => {
|
||||||
|
it("should add a persistent tag to the opposing side of the field", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
||||||
|
|
||||||
|
expect(game).not.toHaveArenaTag(tagType);
|
||||||
|
|
||||||
|
game.move.use(move);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Tag should've been added to the opposing side of the field
|
||||||
|
expect(game).not.toHaveArenaTag(tagType, ArenaTagSide.PLAYER);
|
||||||
|
expect(game).toHaveArenaTag(tagType, ArenaTagSide.ENEMY);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: re-enable after re-fixing hazards moves
|
||||||
|
it.todo("should work when all targets fainted", async () => {
|
||||||
|
game.override.battleStyle("double");
|
||||||
|
await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.SHUCKLE]);
|
||||||
|
|
||||||
|
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||||
|
game.move.use(move, BattlerIndex.PLAYER_2);
|
||||||
|
await game.doKillOpponents();
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemy1.isFainted()).toBe(true);
|
||||||
|
expect(enemy2.isFainted()).toBe(true);
|
||||||
|
expect(game).toHaveArenaTag(tagType, ArenaTagSide.ENEMY);
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxLayers = tagType === ArenaTagType.SPIKES ? 3 : tagType === ArenaTagType.TOXIC_SPIKES ? 2 : 1;
|
||||||
|
const msgText =
|
||||||
|
maxLayers === 1
|
||||||
|
? "should fail if added while already present"
|
||||||
|
: `can be added up to ${maxLayers} times in a row before failing`;
|
||||||
|
|
||||||
|
it(msgText, async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const feebas = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
// set up hazards until at max layers
|
||||||
|
for (let i = 0; i < maxLayers; i++) {
|
||||||
|
game.move.use(move);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(feebas).toHaveUsedMove({ move, result: MoveResult.SUCCESS });
|
||||||
|
expect(game).toHaveArenaTag({ tagType, side: ArenaTagSide.ENEMY, layers: i + 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
game.move.use(move);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(feebas).toHaveUsedMove({ move, result: MoveResult.FAIL });
|
||||||
|
expect(game).toHaveArenaTag({ tagType, side: ArenaTagSide.ENEMY, layers: maxLayers });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Spikes", () => {
|
||||||
|
it.each<{ layers: number; damage: number }>([
|
||||||
|
{ layers: 1, damage: 12.5 },
|
||||||
|
{ layers: 2, damage: 100 / 6 },
|
||||||
|
{ layers: 3, damage: 25 },
|
||||||
|
])("should play message and deal $damage% of the target's max HP at $layers", async ({ layers, damage }) => {
|
||||||
|
for (let i = 0; i < layers; i++) {
|
||||||
|
game.scene.arena.addTag(ArenaTagType.SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
||||||
|
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
expect(enemy).toHaveTakenDamage((enemy.getMaxHp() * damage) / 100);
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("arenaTag:spikesActivateTrap", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(enemy),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Toxic Spikes", () => {
|
||||||
|
it.each<{ name: string; layers: number; status: StatusEffect }>([
|
||||||
|
{ name: "Poison", layers: 1, status: StatusEffect.POISON },
|
||||||
|
{ name: "Toxic", layers: 2, status: StatusEffect.TOXIC },
|
||||||
|
])("should apply $name at $layers without displaying neutralization msg", async ({ layers, status }) => {
|
||||||
|
for (let i = 0; i < layers; i++) {
|
||||||
|
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||||
|
}
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
||||||
|
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
expect(enemy).toHaveStatusEffect(status);
|
||||||
|
expect(game.textInterceptor.logs).not.toContain(
|
||||||
|
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(enemy),
|
||||||
|
moveName: allMoves[MoveId.TOXIC_SPIKES].name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be removed without triggering upon a grounded Poison-type switching in", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]);
|
||||||
|
|
||||||
|
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||||
|
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const ekans = game.field.getPlayerPokemon();
|
||||||
|
expect(game).not.toHaveArenaTag(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER);
|
||||||
|
expect(game.textInterceptor.logs).not.toContain(
|
||||||
|
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(ekans),
|
||||||
|
moveName: allMoves[MoveId.TOXIC_SPIKES].name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(ekans).not.toHaveStatusEffect(StatusEffect.POISON);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Stealth Rock", () => {
|
||||||
|
it.each<{ multi: TypeDamageMultiplier; species: SpeciesId }>([
|
||||||
|
{ multi: 0.25, species: SpeciesId.LUCARIO },
|
||||||
|
{ multi: 0.5, species: SpeciesId.DURALUDON },
|
||||||
|
{ multi: 1, species: SpeciesId.LICKILICKY },
|
||||||
|
{ multi: 2, species: SpeciesId.DARMANITAN },
|
||||||
|
{ multi: 4, species: SpeciesId.DELIBIRD },
|
||||||
|
])("should deal damage based on the target's weakness to Rock - $multi", async ({ multi, species }) => {
|
||||||
|
game.override.enemySpecies(species);
|
||||||
|
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]);
|
||||||
|
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
expect(enemy.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true)).toBe(multi);
|
||||||
|
expect(enemy).toHaveTakenDamage(enemy.getMaxHp() * 0.125 * multi);
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("arenaTag:stealthRockActivateTrap", {
|
||||||
|
pokemonName: getPokemonNameWithAffix(enemy),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore strong winds for type effectiveness", async () => {
|
||||||
|
game.override.enemyAbility(AbilityId.DELTA_STREAM).enemySpecies(SpeciesId.RAYQUAZA);
|
||||||
|
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]);
|
||||||
|
|
||||||
|
const rayquaza = game.field.getEnemyPokemon();
|
||||||
|
// took 25% damage despite strong winds halving effectiveness
|
||||||
|
expect(rayquaza).toHaveTakenDamage(rayquaza.getMaxHp() * 0.25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Sticky Web", () => {
|
||||||
|
it("should lower the target's speed by 1 stage on entry", async () => {
|
||||||
|
game.scene.arena.addTag(ArenaTagType.STICKY_WEB, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]);
|
||||||
|
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
expect(enemy).toHaveStatStage(Stat.SPD, -1);
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("arenaTag:stickyWebActivateTrap", {
|
||||||
|
pokemonName: enemy.getNameToRender(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -44,7 +44,7 @@ describe("Moves - Fusion Flare", () => {
|
|||||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||||
|
|
||||||
// Inflict freeze quietly and check if it was properly inflicted
|
// 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);
|
expect(partyMember.status!.effect).toBe(StatusEffect.FREEZE);
|
||||||
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
146
test/moves/rest.test.ts
Normal file
146
test/moves/rest.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
@ -31,13 +32,36 @@ describe("Moves - Sleep Talk", () => {
|
|||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.NO_GUARD)
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
.enemyMoveset(MoveId.SPLASH)
|
||||||
.enemyLevel(100);
|
.enemyLevel(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail when the user is not asleep", async () => {
|
it("should call a random valid move if the user is asleep", async () => {
|
||||||
game.override.statusEffect(StatusEffect.NONE);
|
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();
|
||||||
|
|
||||||
|
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 fail if the user is not asleep", async () => {
|
||||||
|
game.override.statusEffect(StatusEffect.POISON);
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
game.move.select(MoveId.SLEEP_TALK);
|
game.move.select(MoveId.SLEEP_TALK);
|
||||||
@ -45,6 +69,19 @@ describe("Moves - Sleep Talk", () => {
|
|||||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
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).toHaveUsedMove({ result: MoveResult.FAIL });
|
||||||
|
});
|
||||||
|
|
||||||
it("should fail if the user has no valid moves", async () => {
|
it("should fail if the user has no valid moves", async () => {
|
||||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.METRONOME, MoveId.SOLAR_BEAM]);
|
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.METRONOME, MoveId.SOLAR_BEAM]);
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
@ -54,22 +91,15 @@ describe("Moves - Sleep Talk", () => {
|
|||||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call a random valid move if the user is asleep", async () => {
|
it("should apply secondary effects of the called move", 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
|
game.override.moveset([MoveId.SLEEP_TALK, MoveId.SCALE_SHOT]);
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
game.move.select(MoveId.SLEEP_TALK);
|
game.move.select(MoveId.SLEEP_TALK);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should apply secondary effects of a move", async () => {
|
const feebas = game.field.getPlayerPokemon();
|
||||||
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
|
expect(feebas.getStatStage(Stat.SPD)).toBe(1);
|
||||||
await game.classicMode.startBattle();
|
expect(feebas.getStatStage(Stat.DEF)).toBe(-1);
|
||||||
|
|
||||||
game.move.select(MoveId.SLEEP_TALK);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().isFullHp()).toBeFalsy(); // Wood Hammer recoil effect should be applied
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
import { ArenaTrapTag } from "#data/arena-tag";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
describe("Moves - Spikes", () => {
|
|
||||||
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")
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
|
||||||
.moveset([MoveId.SPIKES, MoveId.SPLASH, MoveId.ROAR]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not damage the team that set them", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
const player = game.field.getPlayerPokemon();
|
|
||||||
expect(player.hp).toBe(player.getMaxHp());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should damage opposing pokemon that are forced to switch in", async () => {
|
|
||||||
game.override.startingWave(5);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.ROAR);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should damage opposing pokemon that choose to switch in", async () => {
|
|
||||||
game.override.startingWave(5);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
game.forceEnemyToSwitch();
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: re-enable after re-fixing hazards moves
|
|
||||||
it.todo("should work when all targets fainted", async () => {
|
|
||||||
game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(1000);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.SHUCKLE]);
|
|
||||||
|
|
||||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
|
||||||
|
|
||||||
game.move.use(MoveId.HYPER_VOICE, BattlerIndex.PLAYER);
|
|
||||||
game.move.use(MoveId.SPIKES, BattlerIndex.PLAYER_2);
|
|
||||||
await game.toEndOfTurn();
|
|
||||||
|
|
||||||
expect(enemy1.isFainted()).toBe(true);
|
|
||||||
expect(enemy2.isFainted()).toBe(true);
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,143 +0,0 @@
|
|||||||
import type { ArenaTrapTag } from "#data/arena-tag";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
|
||||||
import type { SessionSaveData } from "#system/game-data";
|
|
||||||
import { GameData } from "#system/game-data";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import { decrypt, encrypt } from "#utils/data";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
describe("Moves - Toxic Spikes", () => {
|
|
||||||
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")
|
|
||||||
.startingWave(5)
|
|
||||||
.enemySpecies(SpeciesId.RATTATA)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
|
||||||
.moveset([MoveId.TOXIC_SPIKES, MoveId.SPLASH, MoveId.ROAR, MoveId.COURT_CHANGE]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not affect the opponent if they do not switch", async () => {
|
|
||||||
await game.classicMode.runToSummon([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
|
||||||
|
|
||||||
const enemy = game.scene.getEnemyField()[0];
|
|
||||||
|
|
||||||
game.move.select(MoveId.TOXIC_SPIKES);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
|
||||||
expect(enemy.status?.effect).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should poison the opponent if they switch into 1 layer", async () => {
|
|
||||||
await game.classicMode.runToSummon([SpeciesId.MIGHTYENA]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TOXIC_SPIKES);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
game.move.select(MoveId.ROAR);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
const enemy = game.scene.getEnemyField()[0];
|
|
||||||
|
|
||||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
|
||||||
expect(enemy.status?.effect).toBe(StatusEffect.POISON);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should badly poison the opponent if they switch into 2 layers", async () => {
|
|
||||||
await game.classicMode.runToSummon([SpeciesId.MIGHTYENA]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TOXIC_SPIKES);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
game.move.select(MoveId.TOXIC_SPIKES);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
game.move.select(MoveId.ROAR);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
const enemy = game.scene.getEnemyField()[0];
|
|
||||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
|
||||||
expect(enemy.status?.effect).toBe(StatusEffect.TOXIC);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be removed if a grounded poison pokemon switches in", async () => {
|
|
||||||
await game.classicMode.runToSummon([SpeciesId.MUK, SpeciesId.PIDGEY]);
|
|
||||||
|
|
||||||
const muk = game.field.getPlayerPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.TOXIC_SPIKES);
|
|
||||||
await game.toNextTurn();
|
|
||||||
// also make sure the toxic spikes are removed even if the pokemon
|
|
||||||
// that set them up is the one switching in (https://github.com/pagefaultgames/pokerogue/issues/935)
|
|
||||||
game.move.select(MoveId.COURT_CHANGE);
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
expect(muk.isFullHp()).toBe(true);
|
|
||||||
expect(muk.status?.effect).toBeUndefined();
|
|
||||||
expect(game.scene.arena.tags.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shouldn't create multiple layers per use in doubles", async () => {
|
|
||||||
await game.classicMode.runToSummon([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TOXIC_SPIKES);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
const arenaTags = game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
|
||||||
expect(arenaTags.tagType).toBe(ArenaTagType.TOXIC_SPIKES);
|
|
||||||
expect(arenaTags.layers).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should persist through reload", async () => {
|
|
||||||
game.override.startingWave(1);
|
|
||||||
const gameData = new GameData();
|
|
||||||
|
|
||||||
await game.classicMode.runToSummon([SpeciesId.MIGHTYENA]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TOXIC_SPIKES);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.doKillOpponents();
|
|
||||||
await game.phaseInterceptor.to("BattleEndPhase");
|
|
||||||
await game.toNextWave();
|
|
||||||
|
|
||||||
const sessionData: SessionSaveData = gameData.getSessionSaveData();
|
|
||||||
localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true));
|
|
||||||
const recoveredData: SessionSaveData = gameData.parseSessionData(
|
|
||||||
decrypt(localStorage.getItem("sessionTestData")!, true),
|
|
||||||
);
|
|
||||||
await gameData.loadSession(0, recoveredData);
|
|
||||||
|
|
||||||
expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags);
|
|
||||||
localStorage.removeItem("sessionTestData");
|
|
||||||
});
|
|
||||||
});
|
|
@ -63,7 +63,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
// Both pokemon fainted
|
// Both pokemon fainted
|
||||||
scene.getPlayerParty().forEach(p => {
|
scene.getPlayerParty().forEach(p => {
|
||||||
p.hp = 0;
|
p.hp = 0;
|
||||||
p.trySetStatus(StatusEffect.FAINT);
|
p.doSetStatus(StatusEffect.FAINT);
|
||||||
void p.updateInfo();
|
void p.updateInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
// Only faint 1st pokemon
|
// Only faint 1st pokemon
|
||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
|
|
||||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
// 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
|
// Only faint 1st pokemon
|
||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
|
|
||||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
// 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
|
// Only faint 1st pokemon
|
||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
|
|
||||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
// 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();
|
const party = scene.getPlayerParty();
|
||||||
party[0].level = 100;
|
party[0].level = 100;
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
party[1].level = 10;
|
party[1].level = 10;
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].level = 10;
|
party[0].level = 10;
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
party[1].level = 100;
|
party[1].level = 100;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag";
|
import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag";
|
||||||
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import type { OneOther } from "#test/@types/test-helpers";
|
import type { OneOther } from "#test/@types/test-helpers";
|
||||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
@ -26,7 +26,7 @@ export function toHaveArenaTag<T extends ArenaTagType>(
|
|||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expectedTag: T | toHaveArenaTagOptions<T>,
|
expectedTag: T | toHaveArenaTagOptions<T>,
|
||||||
side?: ArenaTagSide,
|
side: ArenaTagSide = ArenaTagSide.BOTH,
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isGameManagerInstance(received)) {
|
if (!isGameManagerInstance(received)) {
|
||||||
return {
|
return {
|
||||||
@ -46,22 +46,27 @@ export function toHaveArenaTag<T extends ArenaTagType>(
|
|||||||
// Bangs are ok as we enforce safety via overloads
|
// Bangs are ok as we enforce safety via overloads
|
||||||
// @ts-expect-error - Typescript is being stupid as tag type and side will always exist
|
// @ts-expect-error - Typescript is being stupid as tag type and side will always exist
|
||||||
const etag: Partial<ArenaTag> & { tagType: T; side: ArenaTagSide } =
|
const etag: Partial<ArenaTag> & { tagType: T; side: ArenaTagSide } =
|
||||||
typeof expectedTag === "object" ? expectedTag : { tagType: expectedTag, side: side! };
|
typeof expectedTag === "object" ? expectedTag : { tagType: expectedTag, side };
|
||||||
|
|
||||||
|
// If checking only tag type/side OR no tags were found, break out early.
|
||||||
// We need to get all tags for the case of checking properties of a tag present on both sides of the arena
|
// We need to get all tags for the case of checking properties of a tag present on both sides of the arena
|
||||||
const tags = received.scene.arena.findTagsOnSide(t => t.tagType === etag.tagType, etag.side);
|
const tags = received.scene.arena.findTagsOnSide(t => t.tagType === etag.tagType, etag.side);
|
||||||
if (tags.length === 0) {
|
if (typeof expectedTag !== "object" || tags.length === 0) {
|
||||||
|
const pass = tags.length > 0;
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass,
|
||||||
message: () => `Expected the Arena to have a tag of type ${etag.tagType}, but it didn't!`,
|
message: () =>
|
||||||
expected: etag.tagType,
|
pass
|
||||||
actual: received.scene.arena.tags.map(t => t.tagType),
|
? `Expected the Arena to NOT have a tag of type ${etag.tagType}, but it did!`
|
||||||
|
: `Expected the Arena to have a tag of type ${etag.tagType}, but it didn't!`,
|
||||||
|
expected: etag,
|
||||||
|
actual: received.scene.arena.tags.map(t => ({ tagType: t.tagType, side: t.side })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass if any of the matching tags meet our criteria
|
// Pass if any of the matching tags meet our criteria
|
||||||
const pass = tags.some(tag =>
|
const pass = tags.some(tag =>
|
||||||
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
this.equals(tag, etag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||||
|
Loading…
Reference in New Issue
Block a user