mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 13:59:27 +02:00
Merge c4df8680fa
into dd03887d05
This commit is contained in:
commit
915e2ae39b
@ -2,7 +2,7 @@ import type { ArenaTagTypeMap } from "#data/arena-tag";
|
||||
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. */
|
||||
export type ArenaTrapTagType =
|
||||
export type EntryHazardTagType =
|
||||
| ArenaTagType.STICKY_WEB
|
||||
| ArenaTagType.SPIKES
|
||||
| ArenaTagType.TOXIC_SPIKES
|
||||
|
@ -6,7 +6,7 @@ import type { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-chang
|
||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
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 { GroundedTag } from "#data/battler-tags";
|
||||
import { getBerryEffectFunc } from "#data/berry";
|
||||
@ -1113,7 +1113,7 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr {
|
||||
}
|
||||
|
||||
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 (
|
||||
this.condition(pokemon, attacker, move) &&
|
||||
(!globalScene.arena.getTag(this.arenaTagType) || tag.layers < tag.maxLayers)
|
||||
|
@ -24,11 +24,11 @@ import type { Pokemon } from "#field/pokemon";
|
||||
import type {
|
||||
ArenaScreenTagType,
|
||||
ArenaTagTypeData,
|
||||
ArenaTrapTagType,
|
||||
EntryHazardTagType,
|
||||
SerializableArenaTagType,
|
||||
} from "#types/arena-tags";
|
||||
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";
|
||||
|
||||
/**
|
||||
@ -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 {
|
||||
abstract readonly tagType: ArenaTrapTagType;
|
||||
public layers: number;
|
||||
public maxLayers: number;
|
||||
|
||||
export abstract class EntryHazardTag extends SerializableArenaTag {
|
||||
public declare abstract readonly tagType: EntryHazardTagType;
|
||||
/**
|
||||
* Creates a new instance of the ArenaTrapTag class.
|
||||
*
|
||||
* @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.
|
||||
* The current number of layers this tag has.
|
||||
* Starts at 1 and increases each time the trap is laid.
|
||||
*/
|
||||
constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide, maxLayers: number) {
|
||||
super(0, sourceMove, sourceId, side);
|
||||
|
||||
this.layers = 1;
|
||||
this.maxLayers = maxLayers;
|
||||
public layers = 1;
|
||||
/** The maximum number of layers this tag can have. */
|
||||
public abstract get maxLayers(): number;
|
||||
/** Whether this tag should only affect grounded targets; default `true` */
|
||||
protected get groundedOnly(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
onOverlap(arena: Arena, _source: Pokemon | null): void {
|
||||
if (this.layers < this.maxLayers) {
|
||||
this.layers++;
|
||||
constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(0, sourceMove, sourceId, side);
|
||||
}
|
||||
|
||||
this.onAdd(arena);
|
||||
// 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the hazard effect onto a Pokemon when it enters the field
|
||||
* @param _arena the {@linkcode Arena} containing this tag
|
||||
* @param simulated if `true`, only checks if the hazard would activate.
|
||||
* @param pokemon the {@linkcode Pokemon} triggering this hazard
|
||||
* 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.onAdd(arena);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the hazard effect onto a Pokemon when it enters the field.
|
||||
* @param _arena - The {@linkcode Arena} at the time of tag activation
|
||||
* @param simulated - Whether to suppress activation effects during execution
|
||||
* @param pokemon - The {@linkcode Pokemon} triggering this hazard
|
||||
* @returns `true` if this hazard affects the given Pokemon; `false` otherwise.
|
||||
*/
|
||||
override apply(_arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
|
||||
@ -768,12 +805,21 @@ export abstract class ArenaTrapTag extends SerializableArenaTag {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.groundedOnly && !pokemon.isGrounded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 {
|
||||
return pokemon.isGrounded()
|
||||
@ -781,141 +827,186 @@ export abstract class ArenaTrapTag extends SerializableArenaTag {
|
||||
: 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);
|
||||
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}.
|
||||
* 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.
|
||||
*/
|
||||
class SpikesTag extends ArenaTrapTag {
|
||||
class SpikesTag extends DamagingTrapTag {
|
||||
public readonly tagType = ArenaTagType.SPIKES;
|
||||
override get maxLayers() {
|
||||
return 3 as const;
|
||||
}
|
||||
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.SPIKES, sourceId, side, 3);
|
||||
super(MoveId.SPIKES, sourceId, side);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
|
||||
// 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(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
protected override getAddMessage(source: Pokemon): string {
|
||||
return i18next.t("arenaTag:spikesOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (!pokemon.isGrounded()) {
|
||||
return false;
|
||||
}
|
||||
protected override getTriggerMessage(pokemon: Pokemon): string {
|
||||
return i18next.t("arenaTag:spikesActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
}
|
||||
|
||||
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),
|
||||
}),
|
||||
);
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
return true;
|
||||
protected override getDamageHpRatio(_pokemon: Pokemon): number {
|
||||
// 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}.
|
||||
* Applies up to 2 layers of Toxic Spikes, poisoning or badly poisoning any Pokémon who is
|
||||
* summoned into this trap if 1 or 2 layers of Toxic Spikes respectively are up. Poison-type
|
||||
* Pokémon summoned into this trap remove it entirely.
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) | Stealth Rock}.
|
||||
* Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon
|
||||
* who is summoned into the trap based on the Rock type's type effectiveness.
|
||||
*/
|
||||
class ToxicSpikesTag extends ArenaTrapTag {
|
||||
#neutralized: boolean;
|
||||
public readonly tagType = ArenaTagType.TOXIC_SPIKES;
|
||||
class StealthRockTag extends DamagingTrapTag {
|
||||
public readonly tagType = ArenaTagType.STEALTH_ROCK;
|
||||
public override get maxLayers() {
|
||||
return 1 as const;
|
||||
}
|
||||
protected override get groundedOnly() {
|
||||
return false;
|
||||
}
|
||||
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.TOXIC_SPIKES, sourceId, side, 2);
|
||||
this.#neutralized = false;
|
||||
super(MoveId.STEALTH_ROCK, sourceId, side);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
|
||||
if (quiet) {
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for ToxicSpikesTag on add message; id: ${this.sourceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
protected override getAddMessage(source: Pokemon): string {
|
||||
return i18next.t("arenaTag:stealthRockOnAdd", {
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
onRemove(arena: Arena): void {
|
||||
protected override getTriggerMessage(pokemon: Pokemon): string {
|
||||
return i18next.t("arenaTag:stealthRockActivateTrap", {
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
}
|
||||
|
||||
protected override getDamageHpRatio(pokemon: Pokemon): number {
|
||||
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(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
// Override remove function to only display text when not neutralized
|
||||
override onRemove(arena: Arena): void {
|
||||
if (!this.#neutralized) {
|
||||
super.onRemove(arena);
|
||||
}
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||
this.#neutralized = true;
|
||||
if (globalScene.arena.removeTag(this.tagType)) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} else if (!pokemon.status) {
|
||||
const toxic = this.layers > 1;
|
||||
if (
|
||||
pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, this.getMoveName())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
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;
|
||||
globalScene.arena.removeTagOnSide(this.tagType, this.side);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt to poison the target, suppressing any status effect messages
|
||||
const effect = this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC;
|
||||
return pokemon.trySetStatus(effect, true, null, 0, this.getMoveName(), false, true);
|
||||
}
|
||||
|
||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||
@ -930,71 +1021,37 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) Stealth Rock}.
|
||||
* Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon
|
||||
* who is summoned into the trap, based on the Rock type's type effectiveness.
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Sticky_Web_(move) | Sticky Web}.
|
||||
* Applies a single-layer trap that lowers the Speed of all grounded Pokémon switching in.
|
||||
*/
|
||||
class StealthRockTag extends ArenaTrapTag {
|
||||
public readonly tagType = ArenaTagType.STEALTH_ROCK;
|
||||
class StickyWebTag extends EntryHazardTag {
|
||||
public readonly tagType = ArenaTagType.STICKY_WEB;
|
||||
public override get maxLayers() {
|
||||
return 1 as const;
|
||||
}
|
||||
|
||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
||||
super(MoveId.STEALTH_ROCK, sourceId, side, 1);
|
||||
super(MoveId.STICKY_WEB, sourceId, side);
|
||||
}
|
||||
|
||||
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;
|
||||
protected override getAddMessage(source: Pokemon): string {
|
||||
return i18next.t("arenaTag:stickyWebOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
// TODO: Does this need to pass `simulated` as a parameter?
|
||||
applyAbAttrs("ProtectStatAbAttr", {
|
||||
pokemon,
|
||||
cancelled,
|
||||
stat: Stat.SPD,
|
||||
stages: -1,
|
||||
});
|
||||
|
||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||
if (!damageHpRatio) {
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1002,95 +1059,96 @@ class StealthRockTag extends ArenaTrapTag {
|
||||
return true;
|
||||
}
|
||||
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:stealthRockActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
i18next.t("arenaTag:stickyWebActivateTrap", {
|
||||
pokemonName: pokemon.getNameToRender(),
|
||||
}),
|
||||
);
|
||||
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));
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
false,
|
||||
[Stat.SPD],
|
||||
-1,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 StickyWebTag extends ArenaTrapTag {
|
||||
public readonly tagType = ArenaTagType.STICKY_WEB;
|
||||
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.STICKY_WEB, sourceId, side, 1);
|
||||
super(MoveId.IMPRISON, sourceId, side);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
/**
|
||||
* Apply the effects of Imprison to all opposing on-field Pokemon.
|
||||
*/
|
||||
override onAdd(_arena: Arena, quiet = false) {
|
||||
super.onAdd(_arena, quiet);
|
||||
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
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(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
return !!source?.isActive(true);
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("ProtectStatAbAttr", {
|
||||
pokemon,
|
||||
cancelled,
|
||||
stat: Stat.SPD,
|
||||
stages: -1,
|
||||
});
|
||||
|
||||
if (simulated) {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
if (!cancelled.value) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:stickyWebActivateTrap", {
|
||||
pokemonName: pokemon.getNameToRender(),
|
||||
}),
|
||||
);
|
||||
const stages = new NumberHolder(-1);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
false,
|
||||
[Stat.SPD],
|
||||
stages.value,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* 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
|
||||
* of {@link https://bulbapedia.bulbagarden.net/wiki/Fire_Pledge_(move) | Fire Pledge}
|
||||
|
@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account";
|
||||
import type { GameMode } from "#app/game-mode";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
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 { MoveChargeAnim } from "#data/battle-anims";
|
||||
import {
|
||||
@ -6083,7 +6083,7 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr {
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
@ -6107,7 +6107,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
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) {
|
||||
globalScene.arena.addTag(this.tagType, 0, move.id, user.id, side);
|
||||
if (!tag) {
|
||||
|
@ -8,7 +8,7 @@ import Overrides from "#app/overrides";
|
||||
import type { BiomeTierTrainerPools, PokemonPools } from "#balance/biomes";
|
||||
import { BiomePoolTier, biomePokemonPools, biomeTrainerPools } from "#balance/biomes";
|
||||
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 type { PokemonSpecies } from "#data/pokemon-species";
|
||||
import { PositionalTagManager } from "#data/positional-tags/positional-tag-manager";
|
||||
@ -709,8 +709,8 @@ export class Arena {
|
||||
if (existingTag) {
|
||||
existingTag.onOverlap(this, globalScene.getPokemonById(sourceId));
|
||||
|
||||
if (existingTag instanceof ArenaTrapTag) {
|
||||
const { tagType, side, turnCount, layers, maxLayers } = existingTag as ArenaTrapTag;
|
||||
if (existingTag instanceof EntryHazardTag) {
|
||||
const { tagType, side, turnCount, layers, maxLayers } = existingTag as EntryHazardTag;
|
||||
this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers));
|
||||
}
|
||||
|
||||
@ -723,7 +723,7 @@ export class Arena {
|
||||
newTag.onAdd(this, quiet);
|
||||
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(
|
||||
new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, layers, maxLayers),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||
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 { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||
@ -584,8 +584,8 @@ export class Trainer extends Phaser.GameObjects.Container {
|
||||
score /= playerField.length;
|
||||
if (forSwitch && !p.isOnField()) {
|
||||
globalScene.arena
|
||||
.findTagsOnSide(t => t instanceof ArenaTrapTag, ArenaTagSide.ENEMY)
|
||||
.map(t => (score *= (t as ArenaTrapTag).getMatchupScoreMultiplier(p)));
|
||||
.findTagsOnSide(t => t instanceof EntryHazardTag, ArenaTagSide.ENEMY)
|
||||
.map(t => (score *= (t as EntryHazardTag).getMatchupScoreMultiplier(p)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
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 { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
@ -16,7 +16,7 @@ export class PostSummonPhase extends PokemonPhase {
|
||||
if (pokemon.status?.effect === StatusEffect.TOXIC) {
|
||||
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 (
|
||||
|
@ -10,7 +10,7 @@ import { Tutorial } from "#app/tutorial";
|
||||
import { speciesEggMoves } from "#balance/egg-moves";
|
||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||
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 type { Egg } from "#data/egg";
|
||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||
@ -1135,8 +1135,8 @@ export class GameData {
|
||||
globalScene.arena.tags = sessionData.arena.tags;
|
||||
if (globalScene.arena.tags) {
|
||||
for (const tag of globalScene.arena.tags) {
|
||||
if (tag instanceof ArenaTrapTag) {
|
||||
const { tagType, side, turnCount, layers, maxLayers } = tag as ArenaTrapTag;
|
||||
if (tag instanceof EntryHazardTag) {
|
||||
const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag;
|
||||
globalScene.arena.eventTarget.dispatchEvent(
|
||||
new TagAddedEvent(tagType, side, turnCount, layers, maxLayers),
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { ArenaTrapTag } from "#data/arena-tag";
|
||||
import { EntryHazardTag } from "#data/arena-tag";
|
||||
import { TerrainType } from "#data/terrain";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
@ -287,7 +287,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||
switch (arenaEffectChangedEvent.constructor) {
|
||||
case 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;
|
||||
|
||||
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 Overrides from "#app/overrides";
|
||||
import type { ArenaTag } from "#data/arena-tag";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ArenaTrapTag } from "#data/arena-tag";
|
||||
import { EntryHazardTag } from "#data/arena-tag";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
@ -50,12 +50,12 @@ describe("Moves - Ceaseless Edge", () => {
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Spikes should not have any layers before move effect is applied
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
||||
expect(tagBefore instanceof ArenaTrapTag).toBeFalsy();
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
||||
expect(tagAfter instanceof ArenaTrapTag).toBeTruthy();
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||
expect(tagAfter.layers).toBe(1);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
});
|
||||
@ -72,12 +72,12 @@ describe("Moves - Ceaseless Edge", () => {
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Spikes should not have any layers before move effect is applied
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
||||
expect(tagBefore instanceof ArenaTrapTag).toBeFalsy();
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
||||
expect(tagAfter instanceof ArenaTrapTag).toBeTruthy();
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||
expect(tagAfter.layers).toBe(2);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
});
|
||||
@ -90,12 +90,12 @@ describe("Moves - Ceaseless Edge", () => {
|
||||
game.move.select(MoveId.CEASELESS_EDGE);
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Spikes should not have any layers before move effect is applied
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
||||
expect(tagBefore instanceof ArenaTrapTag).toBeFalsy();
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||
|
||||
await game.toNextTurn();
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
||||
expect(tagAfter instanceof ArenaTrapTag).toBeTruthy();
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||
expect(tagAfter.layers).toBe(2);
|
||||
|
||||
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 { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
@ -195,7 +195,7 @@ describe("Moves - Destiny Bond", () => {
|
||||
expect(playerPokemon.isFainted()).toBe(true);
|
||||
|
||||
// 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.layers).toBe(1);
|
||||
});
|
||||
@ -220,7 +220,10 @@ describe("Moves - Destiny Bond", () => {
|
||||
expect(playerPokemon1?.isFainted()).toBe(true);
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
|
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(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -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.scene.getPlayerParty()[0];
|
||||
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.scene.getEnemyParty()[0];
|
||||
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.scene.getEnemyParty()[0];
|
||||
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");
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
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 { OneOther } from "#test/@types/test-helpers";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||
@ -26,7 +26,7 @@ export function toHaveArenaTag<T extends ArenaTagType>(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedTag: T | toHaveArenaTagOptions<T>,
|
||||
side?: ArenaTagSide,
|
||||
side: ArenaTagSide = ArenaTagSide.BOTH,
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
@ -46,22 +46,27 @@ export function toHaveArenaTag<T extends ArenaTagType>(
|
||||
// Bangs are ok as we enforce safety via overloads
|
||||
// @ts-expect-error - Typescript is being stupid as tag type and side will always exist
|
||||
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
|
||||
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 {
|
||||
pass: false,
|
||||
message: () => `Expected the Arena to have a tag of type ${etag.tagType}, but it didn't!`,
|
||||
expected: etag.tagType,
|
||||
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `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
|
||||
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);
|
||||
|
Loading…
Reference in New Issue
Block a user