mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-06 07:29:30 +02:00
Cleaned up entry hazard arena tags; merged tests into 1 file
This commit is contained in:
parent
ba48f16500
commit
687a28e85f
@ -28,7 +28,7 @@ import type {
|
|||||||
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,83 +725,36 @@ export class IonDelugeTag extends ArenaTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class to implement arena traps.
|
* Abstract class to implement [arena traps (AKA 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 will stack multiple "layers" of the effect, increasing its severity.
|
||||||
*/
|
*/
|
||||||
export abstract class ArenaTrapTag extends SerializableArenaTag {
|
export abstract class ArenaTrapTag extends SerializableArenaTag {
|
||||||
abstract readonly tagType: ArenaTrapTagType;
|
abstract readonly tagType: ArenaTrapTagType;
|
||||||
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;
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide) {
|
||||||
super(0, sourceMove, sourceId, side);
|
super(0, sourceMove, sourceId, side);
|
||||||
|
|
||||||
this.layers = 1;
|
|
||||||
this.maxLayers = maxLayers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onOverlap(arena: Arena, _source: Pokemon | null): void {
|
// TODO: Add a `canAdd` field to arena tags to remove need for callers to check layer counts
|
||||||
if (this.layers < this.maxLayers) {
|
|
||||||
this.layers++;
|
|
||||||
|
|
||||||
this.onAdd(arena);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates the hazard effect onto a Pokemon when it enters the field
|
* Display text when this tag is added to the field.
|
||||||
* @param _arena the {@linkcode Arena} containing this tag
|
* @param _arena - The {@linkcode Arena} at the time of adding this tag
|
||||||
* @param simulated if `true`, only checks if the hazard would activate.
|
* @param quiet - Whether to suppress messages during tag creation; default `false`
|
||||||
* @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 {
|
override onAdd(_arena: Arena, quiet = false): void {
|
||||||
if ((this.side === ArenaTagSide.PLAYER) !== pokemon.isPlayer()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.activateTrap(pokemon, simulated);
|
|
||||||
}
|
|
||||||
|
|
||||||
activateTrap(_pokemon: Pokemon, _simulated: boolean): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
|
||||||
return pokemon.isGrounded()
|
|
||||||
? 1
|
|
||||||
: 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 {
|
|
||||||
super.loadTag(source);
|
|
||||||
this.layers = source.layers;
|
|
||||||
this.maxLayers = source.maxLayers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
public readonly tagType = ArenaTagType.SPIKES;
|
|
||||||
constructor(sourceId: number | undefined, side: ArenaTagSide) {
|
|
||||||
super(MoveId.SPIKES, sourceId, side, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(arena: Arena, quiet = false): void {
|
|
||||||
super.onAdd(arena);
|
|
||||||
|
|
||||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||||
if (quiet) {
|
if (quiet) {
|
||||||
return;
|
return;
|
||||||
@ -809,113 +762,251 @@ class SpikesTag extends ArenaTrapTag {
|
|||||||
|
|
||||||
const source = this.getSourcePokemon();
|
const source = this.getSourcePokemon();
|
||||||
if (!source) {
|
if (!source) {
|
||||||
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
|
console.warn(
|
||||||
|
"Failed to get source Pokemon for AernaTrapTag on add message!" +
|
||||||
|
`\nTag type: ${this.tagType}` +
|
||||||
|
`\nPID: ${this.sourceId}`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(this.getAddMessage(source));
|
||||||
i18next.t("arenaTag:spikesOnAdd", {
|
|
||||||
moveName: this.getMoveName(),
|
|
||||||
opponentDesc: source.getOpponentDescriptor(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
/**
|
||||||
if (!pokemon.isGrounded()) {
|
* 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 {
|
||||||
|
if ((this.side === ArenaTagSide.PLAYER) !== pokemon.isPlayer()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelled = new BooleanHolder(false);
|
if (this.groundedOnly && !pokemon.isGrounded()) {
|
||||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
return false;
|
||||||
if (simulated || cancelled.value) {
|
|
||||||
return !cancelled.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const damageHpRatio = 1 / (10 - 2 * this.layers);
|
return this.activateTrap(pokemon, simulated);
|
||||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
/**
|
||||||
i18next.t("arenaTag:spikesActivateTrap", {
|
* Activate this trap's effects when a Pokemon switches into it.
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
* @param _pokemon - The {@linkcode Pokemon}
|
||||||
}),
|
* @param _simulated - Whether the activation is simulated
|
||||||
);
|
* @returns Whether the trap activation succeeded
|
||||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
* @todo Do we need the return value? nothing uses it
|
||||||
pokemon.turnData.damageTaken += damage;
|
*/
|
||||||
return true;
|
protected abstract activateTrap(_pokemon: Pokemon, _simulated: boolean): boolean;
|
||||||
|
|
||||||
|
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||||
|
return pokemon.isGrounded()
|
||||||
|
? 1
|
||||||
|
: 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">): void {
|
||||||
|
super.loadTag(source);
|
||||||
|
this.layers = source.layers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Toxic_Spikes_(move) Toxic Spikes}.
|
* Abstract class to implement damaging entry hazards.
|
||||||
* Applies up to 2 layers of Toxic Spikes, poisoning or badly poisoning any Pokémon who is
|
* Currently used for {@linkcode SpikesTag} and {@linkcode StealthRockTag}.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
class ToxicSpikesTag extends ArenaTrapTag {
|
abstract class DamagingTrapTag extends ArenaTrapTag {
|
||||||
#neutralized: boolean;
|
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||||
public readonly tagType = ArenaTagType.TOXIC_SPIKES;
|
// 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 DamagingTrapTag {
|
||||||
|
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.TOXIC_SPIKES, sourceId, side, 2);
|
super(MoveId.SPIKES, sourceId, side);
|
||||||
this.#neutralized = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(arena: Arena, quiet = false): void {
|
protected override getAddMessage(source: Pokemon): string {
|
||||||
super.onAdd(arena);
|
return i18next.t("arenaTag:spikesOnAdd", {
|
||||||
|
moveName: this.getMoveName(),
|
||||||
if (quiet) {
|
opponentDesc: source.getOpponentDescriptor(),
|
||||||
// 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(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(arena: Arena): void {
|
protected override getTriggerMessage(pokemon: Pokemon): string {
|
||||||
|
return i18next.t("arenaTag:spikesActivateTrap", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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/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 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.STEALTH_ROCK, sourceId, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override getAddMessage(source: Pokemon): string {
|
||||||
|
return i18next.t("arenaTag:stealthRockOnAdd", {
|
||||||
|
opponentDesc: source.getOpponentDescriptor(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ArenaTrapTag {
|
||||||
|
/**
|
||||||
|
* 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) {
|
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)) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
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}.
|
* 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 ArenaTrapTag {
|
||||||
public readonly tagType = ArenaTagType.STEALTH_ROCK;
|
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.STEALTH_ROCK, 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", {
|
||||||
|
moveName: this.getMoveName(),
|
||||||
if (quiet) {
|
opponentDesc: source.getOpponentDescriptor(),
|
||||||
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 {
|
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||||
const cancelled = new BooleanHolder(false);
|
const cancelled = new BooleanHolder(false);
|
||||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
// TODO: Does this need to pass `simulated` as a parameter?
|
||||||
if (cancelled.value) {
|
applyAbAttrs("ProtectStatAbAttr", {
|
||||||
return false;
|
pokemon,
|
||||||
}
|
cancelled,
|
||||||
|
stat: Stat.SPD,
|
||||||
|
stages: -1,
|
||||||
|
});
|
||||||
|
|
||||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
if (cancelled.value) {
|
||||||
if (!damageHpRatio) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1002,95 +1059,96 @@ class StealthRockTag extends ArenaTrapTag {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t("arenaTag:stealthRockActivateTrap", {
|
i18next.t("arenaTag:stickyWebActivateTrap", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
pokemonName: pokemon.getNameToRender(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
|
||||||
pokemon.turnData.damageTaken += damage;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
globalScene.phaseManager.unshiftNew(
|
||||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
"StatStageChangePhase",
|
||||||
return Phaser.Math.Linear(super.getMatchupScoreMultiplier(pokemon), 1, 1 - Math.pow(damageHpRatio, damageHpRatio));
|
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}.
|
* This arena tag facilitates the application of the move Imprison
|
||||||
* Applies up to 1 layer of Sticky Web, which lowers the Speed by one stage
|
* Imprison remains in effect as long as the source Pokemon is active and present on the field.
|
||||||
* to any Pokémon who is summoned into this trap.
|
* Imprison will apply to any opposing Pokemon that switch onto the field as well.
|
||||||
*/
|
*/
|
||||||
class StickyWebTag extends ArenaTrapTag {
|
class ImprisonTag extends ArenaTrapTag {
|
||||||
public readonly tagType = ArenaTagType.STICKY_WEB;
|
public readonly tagType = ArenaTagType.IMPRISON;
|
||||||
|
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.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"
|
const party = this.getAffectedPokemon();
|
||||||
if (quiet) {
|
party.forEach(p => {
|
||||||
return;
|
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();
|
const source = this.getSourcePokemon();
|
||||||
if (!source) {
|
return !!source?.isActive(true);
|
||||||
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(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
/**
|
||||||
if (pokemon.isGrounded()) {
|
* This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active
|
||||||
const cancelled = new BooleanHolder(false);
|
* @param {Pokemon} pokemon the Pokemon Imprison is applied to
|
||||||
applyAbAttrs("ProtectStatAbAttr", {
|
* @returns `true`
|
||||||
pokemon,
|
*/
|
||||||
cancelled,
|
override activateTrap(pokemon: Pokemon): boolean {
|
||||||
stat: Stat.SPD,
|
const source = this.getSourcePokemon();
|
||||||
stages: -1,
|
if (source?.isActive(true) && pokemon.isAllowedInBattle()) {
|
||||||
});
|
pokemon.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
* 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}
|
||||||
|
234
test/moves/entry-hazards.test.ts
Normal file
234
test/moves/entry-hazards.test.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { ArenaTrapTag } from "#data/arena-tag";
|
||||||
|
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 { ArenaTrapTagType } 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: ArenaTrapTagType }>([
|
||||||
|
{ 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.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();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
// shoudl
|
||||||
|
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.ARTICUNO },
|
||||||
|
])("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", {
|
||||||
|
pokemonNameWithAffix: 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.scene.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");
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user