Merge remote-tracking branch 'upstream/beta' into future-sight-battle-flyout

This commit is contained in:
Bertie690 2025-09-18 17:45:08 -04:00
commit 5da3d4dced
34 changed files with 237 additions and 150 deletions

View File

@ -85,6 +85,10 @@ interface BaseArenaTag {
* The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite. * The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite.
*/ */
turnCount: number; turnCount: number;
/**
* The tag's max duration.
*/
maxDuration: number;
/** /**
* The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move. * The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move.
*/ */
@ -111,12 +115,14 @@ export abstract class ArenaTag implements BaseArenaTag {
/** The type of the arena tag */ /** The type of the arena tag */
public abstract readonly tagType: ArenaTagType; public abstract readonly tagType: ArenaTagType;
public turnCount: number; public turnCount: number;
public maxDuration: number;
public sourceMove?: MoveId; public sourceMove?: MoveId;
public sourceId: number | undefined; public sourceId: number | undefined;
public side: ArenaTagSide; public side: ArenaTagSide;
constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) { constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) {
this.turnCount = turnCount; this.turnCount = turnCount;
this.maxDuration = turnCount;
this.sourceMove = sourceMove; this.sourceMove = sourceMove;
this.sourceId = sourceId; this.sourceId = sourceId;
this.side = side; this.side = side;
@ -165,6 +171,7 @@ export abstract class ArenaTag implements BaseArenaTag {
*/ */
loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType">): void { loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType">): void {
this.turnCount = source.turnCount; this.turnCount = source.turnCount;
this.maxDuration = source.maxDuration;
this.sourceMove = source.sourceMove; this.sourceMove = source.sourceMove;
this.sourceId = source.sourceId; this.sourceId = source.sourceId;
this.side = source.side; this.side = source.side;

View File

@ -1119,7 +1119,7 @@ export const biomePokemonPools: BiomePokemonPools = {
}, },
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] },
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] }, SpeciesId.GALAR_ZAPDOS ] }, [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] }, SpeciesId.GALAR_ZAPDOS ] },
[BiomePoolTier.BOSS]: { [BiomePoolTier.BOSS]: {
[TimeOfDay.DAWN]: [], [TimeOfDay.DAWN]: [],
[TimeOfDay.DAY]: [], [TimeOfDay.DAY]: [],
@ -1128,7 +1128,7 @@ export const biomePokemonPools: BiomePokemonPools = {
[TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ] [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ]
}, },
[BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] } ] }, [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] } ] },
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] } [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] }
}, },
[BiomeId.FACTORY]: { [BiomeId.FACTORY]: {
@ -1597,10 +1597,10 @@ export const biomePokemonPools: BiomePokemonPools = {
[BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] }, [BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] },
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] },
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] },
[BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] },
[BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] },
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] } [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] }
}, },
[BiomeId.END]: { [BiomeId.END]: {
@ -5627,10 +5627,12 @@ export function initBiomes() {
] ]
], ],
[ SpeciesId.TYPE_NULL, PokemonType.NORMAL, -1, [ [ SpeciesId.TYPE_NULL, PokemonType.NORMAL, -1, [
[ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ] [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
[ SpeciesId.SILVALLY, PokemonType.NORMAL, -1, [ [ SpeciesId.SILVALLY, PokemonType.NORMAL, -1, [
[ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ] [ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
@ -5773,10 +5775,12 @@ export function initBiomes() {
] ]
], ],
[ SpeciesId.POIPOLE, PokemonType.POISON, -1, [ [ SpeciesId.POIPOLE, PokemonType.POISON, -1, [
[ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ] [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
[ SpeciesId.NAGANADEL, PokemonType.POISON, PokemonType.DRAGON, [ [ SpeciesId.NAGANADEL, PokemonType.POISON, PokemonType.DRAGON, [
[ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ] [ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
@ -6165,10 +6169,12 @@ export function initBiomes() {
] ]
], ],
[ SpeciesId.KUBFU, PokemonType.FIGHTING, -1, [ [ SpeciesId.KUBFU, PokemonType.FIGHTING, -1, [
[ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ] [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
[ SpeciesId.URSHIFU, PokemonType.FIGHTING, PokemonType.DARK, [ [ SpeciesId.URSHIFU, PokemonType.FIGHTING, PokemonType.DARK, [
[ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ] [ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
@ -7209,7 +7215,8 @@ export function initBiomes() {
], ],
[ TrainerType.SCIENTIST, [ [ TrainerType.SCIENTIST, [
[ BiomeId.DESERT, BiomePoolTier.COMMON ], [ BiomeId.DESERT, BiomePoolTier.COMMON ],
[ BiomeId.RUINS, BiomePoolTier.COMMON ] [ BiomeId.RUINS, BiomePoolTier.COMMON ],
[ BiomeId.LABORATORY, BiomePoolTier.COMMON ]
] ]
], ],
[ TrainerType.SMASHER, []], [ TrainerType.SMASHER, []],
@ -7224,7 +7231,8 @@ export function initBiomes() {
] ]
], ],
[ TrainerType.SWIMMER, [ [ TrainerType.SWIMMER, [
[ BiomeId.SEA, BiomePoolTier.COMMON ] [ BiomeId.SEA, BiomePoolTier.COMMON ],
[ BiomeId.SEABED, BiomePoolTier.COMMON ]
] ]
], ],
[ TrainerType.TWINS, [ [ TrainerType.TWINS, [
@ -7590,11 +7598,13 @@ export function initBiomes() {
[ TrainerType.ALDER, []], [ TrainerType.ALDER, []],
[ TrainerType.IRIS, []], [ TrainerType.IRIS, []],
[ TrainerType.DIANTHA, []], [ TrainerType.DIANTHA, []],
[ TrainerType.KUKUI, []],
[ TrainerType.HAU, []], [ TrainerType.HAU, []],
[ TrainerType.LEON, []],
[ TrainerType.MUSTARD, []],
[ TrainerType.GEETA, []], [ TrainerType.GEETA, []],
[ TrainerType.NEMONA, []], [ TrainerType.NEMONA, []],
[ TrainerType.KIERAN, []], [ TrainerType.KIERAN, []],
[ TrainerType.LEON, []],
[ TrainerType.RIVAL, []] [ TrainerType.RIVAL, []]
]; ];

View File

@ -22,10 +22,12 @@ export interface SerializedTerrain {
export class Terrain { export class Terrain {
public terrainType: TerrainType; public terrainType: TerrainType;
public turnsLeft: number; public turnsLeft: number;
public maxDuration: number;
constructor(terrainType: TerrainType, turnsLeft?: number) { constructor(terrainType: TerrainType, turnsLeft = 0, maxDuration: number = turnsLeft) {
this.terrainType = terrainType; this.terrainType = terrainType;
this.turnsLeft = turnsLeft || 0; this.turnsLeft = turnsLeft;
this.maxDuration = maxDuration;
} }
lapse(): boolean { lapse(): boolean {

View File

@ -19,10 +19,12 @@ export interface SerializedWeather {
export class Weather { export class Weather {
public weatherType: WeatherType; public weatherType: WeatherType;
public turnsLeft: number; public turnsLeft: number;
public maxDuration: number;
constructor(weatherType: WeatherType, turnsLeft?: number) { constructor(weatherType: WeatherType, turnsLeft = 0, maxDuration: number = turnsLeft) {
this.weatherType = weatherType; this.weatherType = weatherType;
this.turnsLeft = !this.isImmutable() ? turnsLeft || 0 : 0; this.turnsLeft = this.isImmutable() ? 0 : turnsLeft;
this.maxDuration = this.isImmutable() ? 0 : maxDuration;
} }
lapse(): boolean { lapse(): boolean {

View File

@ -28,18 +28,24 @@ export class WeatherChangedEvent extends ArenaEvent {
declare type: typeof ArenaEventType.WEATHER_CHANGED; declare type: typeof ArenaEventType.WEATHER_CHANGED;
/** The new {@linkcode WeatherType} being set. */ /** The new {@linkcode WeatherType} being set. */
public weatherType: WeatherType; public readonly weatherType: WeatherType;
/** /**
* The new weather's initial duration. * The new weather's current duration.
* Unused if {@linkcode weatherType} is set to {@linkcode WeatherType.NONE}. * Unused if {@linkcode weatherType} is set to {@linkcode WeatherType.NONE}.
*/ */
public duration: number; public readonly duration: number;
/**
* The new weather's maximum duration.
* Unused if {@linkcode weatherType} is set to {@linkcode WeatherType.NONE}.
*/
public readonly maxDuration: number;
constructor(weatherType: WeatherType, duration: number) { constructor(weatherType: WeatherType, duration: number, maxDuration = duration) {
super(ArenaEventType.WEATHER_CHANGED); super(ArenaEventType.WEATHER_CHANGED);
this.weatherType = weatherType; this.weatherType = weatherType;
this.duration = duration; this.duration = duration;
this.maxDuration = maxDuration;
} }
} }
@ -52,18 +58,24 @@ export class TerrainChangedEvent extends ArenaEvent {
declare type: typeof ArenaEventType.TERRAIN_CHANGED; declare type: typeof ArenaEventType.TERRAIN_CHANGED;
/** The new {@linkcode TerrainType} being set. */ /** The new {@linkcode TerrainType} being set. */
public terrainType: TerrainType; public readonly terrainType: TerrainType;
/** /**
* The new terrain's initial duration. * The new terrain's current duration.
* Unused if {@linkcode terrainType} is set to {@linkcode TerrainType.NONE}. * Unused if {@linkcode terrainType} is set to {@linkcode TerrainType.NONE}.
*/ */
public duration: number; public readonly duration: number;
/**
* The new terrain's maximum duration.
* Unused if {@linkcode weatherType} is set to {@linkcode WeatherType.NONE}.
*/
public readonly maxDuration: number;
constructor(terrainType: TerrainType, duration: number) { constructor(terrainType: TerrainType, duration: number, maxDuration = duration) {
super(ArenaEventType.TERRAIN_CHANGED); super(ArenaEventType.TERRAIN_CHANGED);
this.terrainType = terrainType; this.terrainType = terrainType;
this.duration = duration; this.duration = duration;
this.maxDuration = maxDuration;
} }
} }
@ -77,28 +89,32 @@ export class ArenaTagAddedEvent extends ArenaEvent {
declare type: typeof ArenaEventType.ARENA_TAG_ADDED; declare type: typeof ArenaEventType.ARENA_TAG_ADDED;
/** The {@linkcode ArenaTagType} of the tag being added */ /** The {@linkcode ArenaTagType} of the tag being added */
public tagType: ArenaTagType; public readonly tagType: ArenaTagType;
/** The {@linkcode ArenaTagSide} the tag is being added too */ /** The {@linkcode ArenaTagSide} the tag is being added too */
public side: ArenaTagSide; public readonly side: ArenaTagSide;
/** The tag's initial duration. */ /** The tag's initial duration. */
public duration: number; public readonly duration: number;
/** The tag's maximum duration. */
public readonly maxDuration: number;
/** /**
* A tuple containing the current and maximum number of layers of the current {@linkcode ArenaTrapTag}, * A tuple containing the current and maximum number of layers of the current {@linkcode ArenaTrapTag},
* or `undefined` if the tag was not a trap. * or `undefined` if the tag was not a trap.
*/ */
public trapLayers: [current: number, max: number] | undefined; public readonly trapLayers: [current: number, max: number] | undefined;
constructor( constructor(
side: ArenaTagType, side: ArenaTagType,
arenaTagSide: ArenaTagSide, arenaTagSide: ArenaTagSide,
duration: number, duration: number,
trapLayers?: [current: number, max: number], trapLayers?: [current: number, max: number],
maxDuration = duration,
) { ) {
super(ArenaEventType.ARENA_TAG_ADDED); super(ArenaEventType.ARENA_TAG_ADDED);
this.tagType = side; this.tagType = side;
this.side = arenaTagSide; this.side = arenaTagSide;
this.duration = duration; this.duration = duration;
this.maxDuration = maxDuration;
this.trapLayers = trapLayers; this.trapLayers = trapLayers;
} }
} }
@ -112,9 +128,9 @@ export class ArenaTagRemovedEvent extends ArenaEvent {
declare type: typeof ArenaEventType.ARENA_TAG_REMOVED; declare type: typeof ArenaEventType.ARENA_TAG_REMOVED;
/** The {@linkcode ArenaTagType} of the tag being removed. */ /** The {@linkcode ArenaTagType} of the tag being removed. */
public tagType: ArenaTagType; public readonly tagType: ArenaTagType;
/** The {@linkcode ArenaTagSide} the removed tag affected. */ /** The {@linkcode ArenaTagSide} the removed tag affected. */
public side: ArenaTagSide; public readonly side: ArenaTagSide;
constructor(tagType: ArenaTagType, side: ArenaTagSide) { constructor(tagType: ArenaTagType, side: ArenaTagSide) {
super(ArenaEventType.ARENA_TAG_REMOVED); super(ArenaEventType.ARENA_TAG_REMOVED);

View File

@ -352,7 +352,7 @@ export class Arena {
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration);
} }
this.weather = weather ? new Weather(weather, weatherDuration.value) : null; this.weather = weather ? new Weather(weather, weatherDuration.value, weatherDuration.value) : null;
this.eventTarget.dispatchEvent(new WeatherChangedEvent(this.getWeatherType(), weatherDuration.value)); this.eventTarget.dispatchEvent(new WeatherChangedEvent(this.getWeatherType(), weatherDuration.value));
if (this.weather) { if (this.weather) {
@ -431,7 +431,7 @@ export class Arena {
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration);
} }
this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null; this.terrain = terrain ? new Terrain(terrain, terrainDuration.value, terrainDuration.value) : null;
this.eventTarget.dispatchEvent(new TerrainChangedEvent(this.getTerrainType(), terrainDuration.value)); this.eventTarget.dispatchEvent(new TerrainChangedEvent(this.getTerrainType(), terrainDuration.value));
@ -719,15 +719,9 @@ export class Arena {
newTag.onAdd(this, quiet); newTag.onAdd(this, quiet);
this.tags.push(newTag); this.tags.push(newTag);
// Dispatch a TagAddedEvent to update the flyout. this.eventTarget.dispatchEvent(
if (newTag instanceof EntryHazardTag) { new ArenaTagAddedEvent(tagType, side, turnCount)
globalScene.arena.eventTarget.dispatchEvent( );
new ArenaTagAddedEvent(tagType, side, turnCount, [newTag.layers, newTag.maxLayers]),
);
} else {
globalScene.arena.eventTarget.dispatchEvent(new ArenaTagAddedEvent(tagType, side, turnCount));
}
return true; return true;
} }

View File

@ -47,8 +47,12 @@ export class ArenaData {
} }
this.biome = source.biome; this.biome = source.biome;
this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null; this.weather = source.weather
this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null; ? new Weather(source.weather.weatherType, source.weather.turnsLeft, source.weather.maxDuration)
: null;
this.terrain = source.terrain
? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft, source.terrain.maxDuration)
: null;
this.positionalTags = source.positionalTags ?? []; this.positionalTags = source.positionalTags ?? [];
} }
} }

View File

@ -15,7 +15,6 @@ import type { Egg } from "#data/egg";
import { pokemonFormChanges } from "#data/pokemon-forms"; import { pokemonFormChanges } from "#data/pokemon-forms";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
import { loadPositionalTag } from "#data/positional-tags/load-positional-tag"; import { loadPositionalTag } from "#data/positional-tags/load-positional-tag";
import { TerrainType } from "#data/terrain";
import { AbilityAttr } from "#enums/ability-attr"; import { AbilityAttr } from "#enums/ability-attr";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { ChallengeType } from "#enums/challenge-type"; import { ChallengeType } from "#enums/challenge-type";
@ -31,8 +30,7 @@ import { StatusEffect } from "#enums/status-effect";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { Unlockables } from "#enums/unlockables"; import { Unlockables } from "#enums/unlockables";
import { WeatherType } from "#enums/weather-type"; import { ArenaTagAddedEvent, WeatherChangedEvent } from "#events/arena";
import { ArenaTagAddedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#events/arena";
import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
// biome-ignore lint/performance/noNamespaceImport: Something weird is going on here and I don't want to touch it // biome-ignore lint/performance/noNamespaceImport: Something weird is going on here and I don't want to touch it
import * as Modifier from "#modifiers/modifier"; import * as Modifier from "#modifiers/modifier";
@ -1016,26 +1014,35 @@ export class GameData {
}); });
globalScene.arena.weather = fromSession.arena.weather; globalScene.arena.weather = fromSession.arena.weather;
if (globalScene.arena.getWeatherType() !== WeatherType.NONE) { if (fromSession.arena.weather != null) {
globalScene.arena.eventTarget.dispatchEvent( globalScene.arena.eventTarget.dispatchEvent(
new WeatherChangedEvent(globalScene.arena.getWeatherType(), globalScene.arena.weather!.turnsLeft), new WeatherChangedEvent(
fromSession.arena.weather.weatherType,
fromSession.arena.weather.turnsLeft,
fromSession.arena.weather.maxDuration,
),
); );
} }
globalScene.arena.terrain = fromSession.arena.terrain; globalScene.arena.terrain = fromSession.arena.terrain;
if (globalScene.arena.getTerrainType() !== TerrainType.NONE) { if (fromSession.arena.terrain != null) {
globalScene.arena.eventTarget.dispatchEvent( globalScene.arena.eventTarget.dispatchEvent(
new TerrainChangedEvent(globalScene.arena.getTerrainType(), globalScene.arena.terrain!.turnsLeft), new WeatherChangedEvent(
fromSession.arena.terrain.terrainType,
fromSession.arena.terrain.turnsLeft,
fromSession.arena.terrain.maxDuration,
),
); );
} }
globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed; globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed;
globalScene.arena.tags = fromSession.arena.tags; globalScene.arena.tags = fromSession.arena.tags;
for (const tag of globalScene.arena.tags) { for (const tag of globalScene.arena.tags) {
const { tagType, side, turnCount } = tag; const { tagType, side, turnCount, maxDuration } = tag;
const layers: [number, number] | undefined = const layers: [number, number] | undefined =
tag instanceof EntryHazardTag ? [tag.layers, tag.maxLayers] : undefined; tag instanceof EntryHazardTag ? [tag.layers, tag.maxLayers] : undefined;
globalScene.arena.eventTarget.dispatchEvent(new ArenaTagAddedEvent(tagType, side, turnCount, layers)); globalScene.arena.eventTarget.dispatchEvent(new ArenaTagAddedEvent(tagType, side, turnCount, layers, maxDuration));
} }
globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag => globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag =>

View File

@ -276,7 +276,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
this.arenaTags.push({ this.arenaTags.push({
name, name,
side: event.side, side: event.side,
maxDuration: event.duration, maxDuration: event.maxDuration,
duration: event.duration, duration: event.duration,
tagType: event.tagType, tagType: event.tagType,
}); });
@ -328,7 +328,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
this.weatherInfo = { this.weatherInfo = {
name: this.localizeEffectName(WeatherType[event.weatherType]), name: this.localizeEffectName(WeatherType[event.weatherType]),
maxDuration: event.duration, maxDuration: event.maxDuration,
duration: event.duration, duration: event.duration,
weatherType: event.weatherType, weatherType: event.weatherType,
}; };
@ -350,7 +350,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
this.terrainInfo = { this.terrainInfo = {
name: this.localizeEffectName(TerrainType[event.terrainType]), name: this.localizeEffectName(TerrainType[event.terrainType]),
maxDuration: event.duration, maxDuration: event.maxDuration,
duration: event.duration, duration: event.duration,
terrainType: event.terrainType, terrainType: event.terrainType,
}; };

View File

@ -17,19 +17,17 @@ export function removeCookie(cName: string): void {
export function getCookie(cName: string): string { export function getCookie(cName: string): string {
// check if there are multiple cookies with the same name and delete them // check if there are multiple cookies with the same name and delete them
if (document.cookie.split(";").filter(c => c.includes(cName)).length > 1) { if (document.cookie.split(";").filter(c => c.trim().includes(cName)).length > 1) {
removeCookie(cName); removeCookie(cName);
return ""; return "";
} }
const name = `${cName}=`; const name = `${cName}=`;
const ca = document.cookie.split(";"); const cookieArray = document.cookie.split(";");
for (let c of ca) { // Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up
// ⚠️ DO NOT REPLACE THIS WITH C = C.TRIM() - IT BREAKS IN NON-CHROMIUM BROWSERS ⚠️ for (const cookie of cookieArray) {
while (c.charAt(0) === " ") { const cookieTrimmed = cookie.trim();
c = c.substring(1); if (cookieTrimmed.startsWith(name)) {
} return cookieTrimmed.slice(name.length, cookieTrimmed.length);
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
} }
} }
return ""; return "";

View File

@ -30,12 +30,13 @@ describe("Ability Duplication", () => {
.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH);
}); });
// TODO: Find a cleaner way of checking ability duplication effects than suppressing the ability
it("huge power should only be applied once if both normal and passive", async () => { it("huge power should only be applied once if both normal and passive", async () => {
game.override.passiveAbility(AbilityId.HUGE_POWER); game.override.passiveAbility(AbilityId.HUGE_POWER);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const [magikarp] = game.scene.getPlayerField(); const magikarp = game.field.getPlayerPokemon();
const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK); const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK);
magikarp.summonData.abilitySuppressed = true; magikarp.summonData.abilitySuppressed = true;
@ -48,7 +49,7 @@ describe("Ability Duplication", () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const [magikarp] = game.scene.getPlayerField(); const magikarp = game.field.getPlayerPokemon();
const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK); const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK);
magikarp.summonData.abilitySuppressed = true; magikarp.summonData.abilitySuppressed = true;

View File

@ -5,8 +5,7 @@ import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import type { EffectiveStat } from "#enums/stat"; import { EFFECTIVE_STATS } from "#enums/stat";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { GameManager } from "#test/test-utils/game-manager"; import { GameManager } from "#test/test-utils/game-manager";
@ -48,23 +47,24 @@ describe("Abilities - Commander", () => {
const [tatsugiri, dondozo] = game.scene.getPlayerField(); const [tatsugiri, dondozo] = game.scene.getPlayerField();
const affectedStats: EffectiveStat[] = [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD];
expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY);
expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); expect(dondozo).toHaveBattlerTag(BattlerTagType.COMMANDED);
affectedStats.forEach(stat => expect(dondozo.getStatStage(stat)).toBe(2)); EFFECTIVE_STATS.forEach(stat => {
expect(dondozo).toHaveStatStage(stat, 2);
game.move.select(MoveId.SPLASH, 1); });
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy();
// Force both enemies to target the Tatsugiri // Force both enemies to target the Tatsugiri
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER);
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER);
await game.phaseInterceptor.to("BerryPhase", false); await game.toEndOfTurn();
game.scene.getEnemyField().forEach(enemy => expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS)); const [enemy1, enemy2] = game.scene.getEnemyField();
expect(tatsugiri.isFullHp()).toBeTruthy(); expect(enemy1).toHaveUsedMove({ move: MoveId.TACKLE, result: MoveResult.MISS });
expect(enemy2).toHaveUsedMove({ move: MoveId.TACKLE, result: MoveResult.MISS });
expect(tatsugiri).toHaveFullHp();
}); });
it("should activate when a Dondozo switches in and cancel the source's move", async () => { it("should activate when a Dondozo switches in and cancel the source's move", async () => {
@ -72,7 +72,7 @@ describe("Abilities - Commander", () => {
await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]); await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]);
const tatsugiri = game.scene.getPlayerField()[0]; const [tatsugiri, _, dondozo] = game.scene.getPlayerParty();
game.move.select(MoveId.LIQUIDATION, 0, BattlerIndex.ENEMY); game.move.select(MoveId.LIQUIDATION, 0, BattlerIndex.ENEMY);
game.doSwitchPokemon(2); game.doSwitchPokemon(2);
@ -80,12 +80,11 @@ describe("Abilities - Commander", () => {
await game.phaseInterceptor.to("MovePhase", false); await game.phaseInterceptor.to("MovePhase", false);
expect(game.scene.triggerPokemonBattleAnim).toHaveBeenCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); expect(game.scene.triggerPokemonBattleAnim).toHaveBeenCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY);
const dondozo = game.scene.getPlayerField()[1];
expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined();
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(tatsugiri.getMoveHistory()).toHaveLength(0); expect(tatsugiri.getMoveHistory()).toHaveLength(0);
expect(game.scene.getEnemyField()[0].isFullHp()).toBeTruthy(); expect(game.field.getEnemyPokemon()).toHaveFullHp();
}); });
it("source should reenter the field when Dondozo faints", async () => { it("source should reenter the field when Dondozo faints", async () => {
@ -192,26 +191,26 @@ describe("Abilities - Commander", () => {
}); });
it("should interrupt the source's semi-invulnerability", async () => { it("should interrupt the source's semi-invulnerability", async () => {
game.override.moveset([MoveId.SPLASH, MoveId.DIVE]).enemyMoveset(MoveId.SPLASH);
await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]); await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]);
const tatsugiri = game.scene.getPlayerField()[0]; const [tatsugiri, , dondozo] = game.scene.getPlayerParty();
game.move.select(MoveId.DIVE, 0, BattlerIndex.ENEMY); game.move.use(MoveId.DIVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, 1); game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeDefined(); expect(tatsugiri).toHaveBattlerTag(BattlerTagType.UNDERWATER);
game.doSwitchPokemon(2); game.doSwitchPokemon(2);
await game.phaseInterceptor.to("MovePhase", false); await game.phaseInterceptor.to("MovePhase", false);
const dondozo = game.scene.getPlayerField()[1];
expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined();
await game.toNextTurn(); expect(tatsugiri).not.toHaveBattlerTag(BattlerTagType.UNDERWATER);
const enemy = game.scene.getEnemyField()[0]; expect(dondozo).toHaveBattlerTag(BattlerTagType.COMMANDED);
expect(enemy.isFullHp()).toBeTruthy();
await game.toEndOfTurn();
expect(game.field.getEnemyPokemon()).toHaveFullHp();
}); });
}); });

View File

@ -74,8 +74,8 @@ describe("Abilities - Dancer", () => {
.enemyLevel(10); .enemyLevel(10);
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
const [oricorio] = game.scene.getPlayerField(); const oricorio = game.field.getPlayerPokemon();
const [, shuckle2] = game.scene.getEnemyField(); const shuckle2 = game.scene.getEnemyField()[1];
game.move.select(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2); game.move.select(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2);
game.move.select(MoveId.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); game.move.select(MoveId.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);

View File

@ -58,12 +58,12 @@ describe("Abilities - Flower Gift", () => {
const ally_target = allyAttacker ? BattlerIndex.ENEMY : null; const ally_target = allyAttacker ? BattlerIndex.ENEMY : null;
await game.classicMode.startBattle([SpeciesId.CHERRIM, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.CHERRIM, SpeciesId.MAGIKARP]);
const target = allyAttacker ? game.scene.getEnemyField()[0] : game.scene.getPlayerField()[1]; const target = allyAttacker ? game.field.getEnemyPokemon() : game.scene.getPlayerField()[1];
const initialHp = target.getMaxHp(); const initialHp = target.getMaxHp();
// Override the ability for the target and attacker only // Override the ability for the target and attacker only
vi.spyOn(game.scene.getPlayerField()[1], "getAbility").mockReturnValue(allAbilities[allyAbility]); vi.spyOn(game.scene.getPlayerField()[1], "getAbility").mockReturnValue(allAbilities[allyAbility]);
vi.spyOn(game.scene.getEnemyField()[0], "getAbility").mockReturnValue(allAbilities[enemyAbility]); vi.spyOn(game.field.getEnemyPokemon(), "getAbility").mockReturnValue(allAbilities[enemyAbility]);
// turn 1 // turn 1
game.move.select(MoveId.SUNNY_DAY, 0); game.move.select(MoveId.SUNNY_DAY, 0);

View File

@ -66,7 +66,7 @@ describe("Abilities - Flower Veil", () => {
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BULBASAUR]);
// Clear the ability of the ally to isolate the test // Clear the ability of the ally to isolate the test
const ally = game.scene.getPlayerField()[1]!; const ally = game.scene.getPlayerField()[1];
vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[AbilityId.BALL_FETCH]); vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[AbilityId.BALL_FETCH]);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);

View File

@ -76,7 +76,7 @@ describe("Abilities - Forecast", () => {
vi.spyOn(game.scene.getPlayerParty()[5], "getAbility").mockReturnValue(allAbilities[AbilityId.CLOUD_NINE]); vi.spyOn(game.scene.getPlayerParty()[5], "getAbility").mockReturnValue(allAbilities[AbilityId.CLOUD_NINE]);
const castform = game.scene.getPlayerField()[0]; const castform = game.field.getPlayerPokemon();
expect(castform.formIndex).toBe(NORMAL_FORM); expect(castform.formIndex).toBe(NORMAL_FORM);
game.move.select(MoveId.RAIN_DANCE); game.move.select(MoveId.RAIN_DANCE);

View File

@ -64,7 +64,7 @@ describe("Abilities - Magic Bounce", () => {
game.move.use(MoveId.SPLASH, 1); game.move.use(MoveId.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
const user = game.scene.getPlayerField()[0]; const user = game.field.getPlayerPokemon();
expect(user.getStatStage(Stat.ATK)).toBe(-2); expect(user.getStatStage(Stat.ATK)).toBe(-2);
}); });

View File

@ -92,8 +92,7 @@ describe("Ability - Mirror Armor", () => {
game.override.battleStyle("double").enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE); game.override.battleStyle("double").enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
const [enemy1, enemy2] = game.scene.getEnemyField(); const [player1, player2, enemy1, enemy2] = game.scene.getField();
const [player1, player2] = game.scene.getPlayerField();
// Enemy has intimidate, enemy should lose -1 atk // Enemy has intimidate, enemy should lose -1 atk
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);

View File

@ -58,6 +58,6 @@ describe("Abilities - No Guard", () => {
await game.classicMode.startBattle(); await game.classicMode.startBattle();
expect(game.scene.getEnemyField().length).toBe(2); expect(game.scene.getEnemyField()).toHaveLength(2);
}); });
}); });

View File

@ -37,9 +37,7 @@ describe("Abilities - Storm Drain", () => {
it("should redirect water type moves", async () => { it("should redirect water type moves", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
const enemy1 = game.scene.getEnemyField()[0]; const [enemy1, enemy2] = game.scene.getEnemyField();
const enemy2 = game.scene.getEnemyField()[1];
game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);
game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
@ -53,8 +51,7 @@ describe("Abilities - Storm Drain", () => {
game.override.moveset([MoveId.SPLASH, MoveId.AERIAL_ACE]); game.override.moveset([MoveId.SPLASH, MoveId.AERIAL_ACE]);
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
const enemy1 = game.scene.getEnemyField()[0]; const [enemy1, enemy2] = game.scene.getEnemyField();
const enemy2 = game.scene.getEnemyField()[1];
game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);
@ -83,8 +80,7 @@ describe("Abilities - Storm Drain", () => {
game.override.ability(AbilityId.NORMALIZE); game.override.ability(AbilityId.NORMALIZE);
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
const enemy1 = game.scene.getEnemyField()[0]; const [enemy1, enemy2] = game.scene.getEnemyField();
const enemy2 = game.scene.getEnemyField()[1];
game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);
game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
@ -98,8 +94,7 @@ describe("Abilities - Storm Drain", () => {
game.override.ability(AbilityId.LIQUID_VOICE); game.override.ability(AbilityId.LIQUID_VOICE);
await game.classicMode.startBattle([SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const enemy1 = game.scene.getEnemyField()[0]; const [enemy1, enemy2] = game.scene.getEnemyField();
const enemy2 = game.scene.getEnemyField()[1];
game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);

View File

@ -362,7 +362,7 @@ describe("Abilities - Unburden", () => {
.startingHeldItems([{ name: "WIDE_LENS" }]); .startingHeldItems([{ name: "WIDE_LENS" }]);
await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.FEEBAS, SpeciesId.MILOTIC]); await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.FEEBAS, SpeciesId.MILOTIC]);
const treecko = game.scene.getPlayerField()[0]; const treecko = game.field.getPlayerPokemon();
const treeckoInitialHeldItems = getHeldItemCount(treecko); const treeckoInitialHeldItems = getHeldItemCount(treecko);
const initialSpeed = treecko.getStat(Stat.SPD); const initialSpeed = treecko.getStat(Stat.SPD);
@ -374,7 +374,7 @@ describe("Abilities - Unburden", () => {
game.doSelectPartyPokemon(0, "RevivalBlessingPhase"); game.doSelectPartyPokemon(0, "RevivalBlessingPhase");
await game.toNextTurn(); await game.toNextTurn();
expect(game.scene.getPlayerField()[0]).toBe(treecko); expect(game.field.getPlayerPokemon()).toBe(treecko);
expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems); expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems);
expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed); expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed);
}); });

View File

@ -7,6 +7,7 @@ import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
// TODO: These tests are stupid and need to be redone
describe("Escape chance calculations", () => { describe("Escape chance calculations", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;

View File

@ -31,7 +31,7 @@ describe("Items - Double Battle Chance Boosters", () => {
await game.classicMode.startBattle(); await game.classicMode.startBattle();
expect(game.scene.getEnemyField().length).toBe(2); expect(game.scene.getEnemyField()).toHaveLength(2);
}); });
it("should guarantee double boss battle with 3 unique tiers", async () => { it("should guarantee double boss battle with 3 unique tiers", async () => {
@ -41,7 +41,7 @@ describe("Items - Double Battle Chance Boosters", () => {
const enemyField = game.scene.getEnemyField(); const enemyField = game.scene.getEnemyField();
expect(enemyField.length).toBe(2); expect(enemyField).toHaveLength(2);
expect(enemyField[0].isBoss()).toBe(true); expect(enemyField[0].isBoss()).toBe(true);
expect(enemyField[1].isBoss()).toBe(true); expect(enemyField[1].isBoss()).toBe(true);
}); });

View File

@ -44,7 +44,7 @@ describe("Items - Grip Claw", () => {
it("should steal items on contact and only from the attack target", async () => { it("should steal items on contact and only from the attack target", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]);
const [playerPokemon] = game.scene.getPlayerField(); const playerPokemon = game.field.getPlayerPokemon();
const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier;
vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100);
@ -73,7 +73,7 @@ describe("Items - Grip Claw", () => {
it("should not steal items when using a targetted, non attack move", async () => { it("should not steal items when using a targetted, non attack move", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]);
const [playerPokemon] = game.scene.getPlayerField(); const playerPokemon = game.field.getPlayerPokemon();
const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier;
vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100);

View File

@ -103,7 +103,7 @@ describe("Items - Multi Lens", () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
const [magikarp] = game.scene.getPlayerField(); const magikarp = game.field.getPlayerPokemon();
game.move.select(MoveId.SWIFT, 0); game.move.select(MoveId.SWIFT, 0);
game.move.select(MoveId.SPLASH, 1); game.move.select(MoveId.SPLASH, 1);

View File

@ -102,7 +102,7 @@ describe("Moves - Ability-Ignoring Moves", () => {
// Both the initial and redirected instruct use ignored sturdy // Both the initial and redirected instruct use ignored sturdy
const [enemy1, enemy2] = game.scene.getEnemyField(); const [enemy1, enemy2] = game.scene.getEnemyField();
expect(enemy1.isFainted()).toBe(true); expect(enemy1).toHaveFainted();
expect(enemy2.isFainted()).toBe(true); expect(enemy2).toHaveFainted();
}); });
}); });

View File

@ -1,4 +1,5 @@
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { ArenaTagType } from "#enums/arena-tag-type";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
@ -32,26 +33,21 @@ describe("Moves - Defog", () => {
.enemyMoveset([MoveId.DEFOG, MoveId.GROWL]); .enemyMoveset([MoveId.DEFOG, MoveId.GROWL]);
}); });
// TODO: Refactor these tests they suck ass
it("should not allow Safeguard to be active", async () => { it("should not allow Safeguard to be active", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const playerPokemon = game.scene.getPlayerField(); game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 0, 0, 0);
const enemyPokemon = game.scene.getEnemyField();
game.move.select(MoveId.SAFEGUARD); game.move.use(MoveId.DEFOG);
await game.move.selectEnemyMove(MoveId.DEFOG); await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase");
expect(playerPokemon[0].isSafeguarded(enemyPokemon[0])).toBe(false); expect(game).not.toHaveArenaTag(ArenaTagType.SAFEGUARD);
expect(true).toBe(true);
}); });
it("should not allow Mist to be active", async () => { it("should not allow Mist to be active", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const playerPokemon = game.scene.getPlayerField();
game.move.select(MoveId.MIST); game.move.select(MoveId.MIST);
await game.move.selectEnemyMove(MoveId.DEFOG); await game.move.selectEnemyMove(MoveId.DEFOG);
@ -62,8 +58,6 @@ describe("Moves - Defog", () => {
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(playerPokemon[0].getStatStage(Stat.ATK)).toBe(-1); expect(game.field.getPlayerPokemon()).toHaveStatStage(Stat.ATK, -1);
expect(true).toBe(true);
}); });
}); });

View File

@ -160,11 +160,7 @@ describe("Moves - Destiny Bond", () => {
game.override.moveset([MoveId.DESTINY_BOND, MoveId.CRUNCH]).battleStyle("double"); game.override.moveset([MoveId.DESTINY_BOND, MoveId.CRUNCH]).battleStyle("double");
await game.classicMode.startBattle([SpeciesId.SHEDINJA, SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE]); await game.classicMode.startBattle([SpeciesId.SHEDINJA, SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE]);
const enemyPokemon0 = game.scene.getEnemyField()[0]; const [playerPokemon0, playerPokemon1, enemyPokemon0, enemyPokemon1] = game.scene.getField();
const enemyPokemon1 = game.scene.getEnemyField()[1];
const playerPokemon0 = game.scene.getPlayerField()[0];
const playerPokemon1 = game.scene.getPlayerField()[1];
// Shedinja uses Destiny Bond, then ally Bulbasaur KO's Shedinja with Crunch // Shedinja uses Destiny Bond, then ally Bulbasaur KO's Shedinja with Crunch
game.move.select(MoveId.DESTINY_BOND, 0); game.move.select(MoveId.DESTINY_BOND, 0);
game.move.select(MoveId.CRUNCH, 1, BattlerIndex.PLAYER); game.move.select(MoveId.CRUNCH, 1, BattlerIndex.PLAYER);

View File

@ -171,7 +171,7 @@ describe("Moves - Dragon Tail", () => {
const enemy = game.field.getEnemyPokemon(); const enemy = game.field.getEnemyPokemon();
expect(enemy).toBeDefined(); expect(enemy).toBeDefined();
expect(enemy.hp).toBe(Math.floor(enemy.getMaxHp() / 2)); expect(enemy.hp).toBe(Math.floor(enemy.getMaxHp() / 2));
expect(game.scene.getEnemyField().length).toBe(1); expect(game.scene.getEnemyField()).toHaveLength(1);
}); });
it("should not cause a softlock when activating a player's reviver seed", async () => { it("should not cause a softlock when activating a player's reviver seed", async () => {

View File

@ -107,7 +107,7 @@ describe("Moves - Fell Stinger", () => {
await game.classicMode.startBattle([SpeciesId.LEAVANNY]); await game.classicMode.startBattle([SpeciesId.LEAVANNY]);
const leadPokemon = game.field.getPlayerPokemon(); const leadPokemon = game.field.getPlayerPokemon();
const leftEnemy = game.scene.getEnemyField()[0]!; const leftEnemy = game.field.getEnemyPokemon();
// Turn 1: set Salt Cure, enemy splashes and does nothing // Turn 1: set Salt Cure, enemy splashes and does nothing
game.move.select(MoveId.SALT_CURE, 0, leftEnemy.getBattlerIndex()); game.move.select(MoveId.SALT_CURE, 0, leftEnemy.getBattlerIndex());

View File

@ -498,7 +498,7 @@ describe("Moves - Instruct", () => {
.enemyLevel(1); .enemyLevel(1);
await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]);
const koraidon = game.scene.getPlayerField()[0]!; const koraidon = game.field.getPlayerPokemon();
game.move.select(MoveId.BREAKING_SWIPE); game.move.select(MoveId.BREAKING_SWIPE);
await game.phaseInterceptor.to("TurnEndPhase", false); await game.phaseInterceptor.to("TurnEndPhase", false);
@ -527,7 +527,7 @@ describe("Moves - Instruct", () => {
.enemyLevel(1); .enemyLevel(1);
await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]);
const koraidon = game.scene.getPlayerField()[0]!; const koraidon = game.field.getPlayerPokemon();
game.move.select(MoveId.BRUTAL_SWING); game.move.select(MoveId.BRUTAL_SWING);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
@ -587,7 +587,7 @@ describe("Moves - Instruct", () => {
.enemyLevel(5); .enemyLevel(5);
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.IVYSAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.IVYSAUR]);
const [, ivysaur] = game.scene.getPlayerField(); const ivysaur = game.scene.getPlayerField()[1];
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);

View File

@ -111,7 +111,8 @@ describe("Moves - Jaw Lock", () => {
await game.classicMode.startBattle([SpeciesId.CHARMANDER, SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.CHARMANDER, SpeciesId.BULBASAUR]);
const playerPokemon = game.scene.getPlayerField(); const playerPokemon = game.field.getPlayerPokemon();
const enemyPokemon = game.scene.getEnemyField(); const enemyPokemon = game.scene.getEnemyField();
game.move.select(MoveId.JAW_LOCK, 0, BattlerIndex.ENEMY); game.move.select(MoveId.JAW_LOCK, 0, BattlerIndex.ENEMY);
@ -120,7 +121,7 @@ describe("Moves - Jaw Lock", () => {
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);
expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined();
expect(enemyPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); expect(enemyPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined();
await game.toNextTurn(); await game.toNextTurn();
@ -131,8 +132,8 @@ describe("Moves - Jaw Lock", () => {
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);
expect(enemyPokemon[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); expect(enemyPokemon[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined();
expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined();
expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id); expect(playerPokemon.getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id);
}); });
it("should not trap either pokemon if the target is protected", async () => { it("should not trap either pokemon if the target is protected", async () => {

View File

@ -34,8 +34,7 @@ describe("Moves - Tailwind", () => {
it("doubles the Speed stat of the Pokemons on its side", async () => { it("doubles the Speed stat of the Pokemons on its side", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MEOWTH]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MEOWTH]);
const magikarp = game.scene.getPlayerField()[0]; const [magikarp, meowth] = game.scene.getPlayerField();
const meowth = game.scene.getPlayerField()[1];
const magikarpSpd = magikarp.getStat(Stat.SPD); const magikarpSpd = magikarp.getStat(Stat.SPD);
const meowthSpd = meowth.getStat(Stat.SPD); const meowthSpd = meowth.getStat(Stat.SPD);

View File

@ -0,0 +1,62 @@
import { getCookie } from "#utils/cookies";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("Unit Tests - cookies.ts", () => {
describe("getCookie", () => {
const cookieStart = document.cookie;
beforeEach(() => {
// clear cookie before each test
document.cookie = "";
});
afterEach(() => {
// restore original cookie after each test
document.cookie = cookieStart;
});
/**
* Spies on `document.cookie` and replaces its value with the provided string.
*/
function setDocumentCookie(value: string) {
vi.spyOn(document, "cookie", "get").mockReturnValue(value);
}
it("returns the value of a single cookie", () => {
setDocumentCookie("foo=bar");
expect(getCookie("foo")).toBe("bar");
});
it("returns empty string if cookie is not found", () => {
setDocumentCookie("foo=bar");
expect(getCookie("baz")).toBe("");
});
it("returns the value when multiple cookies exist", () => {
setDocumentCookie("foo=bar; baz=qux");
expect(getCookie("baz")).toBe("qux");
});
it("trims leading spaces in cookies", () => {
setDocumentCookie("foo=bar; baz=qux");
expect(getCookie("baz")).toBe("qux");
});
it("returns the value of the first matching cookie if only one exists", () => {
setDocumentCookie("foo=bar; test=val");
expect(getCookie("foo")).toBe("bar");
});
it("returns empty string if document.cookie is empty", () => {
setDocumentCookie("");
expect(getCookie("foo")).toBe("");
});
it("handles cookies that aren't separated with a space", () => {
setDocumentCookie("foo=bar;baz=qux;quux=corge;grault=garply");
expect(getCookie("baz")).toBe("qux");
});
it("handles cookies that may have leading tab characters", () => {
setDocumentCookie("foo=bar;\tbaz=qux");
expect(getCookie("baz")).toBe("qux");
});
});
});