Merge branch 'beta' into future-sight

This commit is contained in:
Bertie690 2025-07-22 18:44:29 -04:00 committed by GitHub
commit 0def2279a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1004 additions and 557 deletions

48
src/@types/arena-tags.ts Normal file
View File

@ -0,0 +1,48 @@
import type { ArenaTagTypeMap } from "#data/arena-tag";
import type { ArenaTagType } from "#enums/arena-tag-type";
import type { NonFunctionProperties } from "./type-helpers";
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
export type ArenaTrapTagType =
| ArenaTagType.STICKY_WEB
| ArenaTagType.SPIKES
| ArenaTagType.TOXIC_SPIKES
| ArenaTagType.STEALTH_ROCK
| ArenaTagType.IMPRISON;
/** Subset of {@linkcode ArenaTagType}s that are considered delayed attacks */
export type ArenaDelayedAttackTagType = ArenaTagType.FUTURE_SIGHT | ArenaTagType.DOOM_DESIRE;
/** Subset of {@linkcode ArenaTagType}s that create {@link https://bulbapedia.bulbagarden.net/wiki/Category:Screen-creating_moves | screens}. */
export type ArenaScreenTagType = ArenaTagType.REFLECT | ArenaTagType.LIGHT_SCREEN | ArenaTagType.AURORA_VEIL;
/** Subset of {@linkcode ArenaTagType}s for moves that add protection */
export type TurnProtectArenaTagType =
| ArenaTagType.QUICK_GUARD
| ArenaTagType.WIDE_GUARD
| ArenaTagType.MAT_BLOCK
| ArenaTagType.CRAFTY_SHIELD;
/** Subset of {@linkcode ArenaTagType}s that cannot persist across turns, and thus should not be serialized in {@linkcode SessionSaveData}. */
export type NonSerializableArenaTagType = ArenaTagType.NONE | TurnProtectArenaTagType | ArenaTagType.ION_DELUGE;
/** Subset of {@linkcode ArenaTagType}s that may persist across turns, and thus must be serialized in {@linkcode SessionSaveData}. */
export type SerializableArenaTagType = Exclude<ArenaTagType, NonSerializableArenaTagType>;
/**
* Type-safe representation of the serializable data of an ArenaTag
*/
export type ArenaTagTypeData = NonFunctionProperties<
ArenaTagTypeMap[keyof {
[K in keyof ArenaTagTypeMap as K extends SerializableArenaTagType ? K : never]: ArenaTagTypeMap[K];
}]
>;
/** Dummy, typescript-only declaration to ensure that
* {@linkcode ArenaTagTypeMap} has a map for all ArenaTagTypes.
*
* If an arena tag is missing from the map, typescript will throw an error on this statement.
*
* Does not actually exist at runtime, so it must not be used!
*/
declare const EnsureAllArenaTagTypesAreMapped: ArenaTagTypeMap[ArenaTagType] & never;

View File

@ -44,3 +44,34 @@ export type Mutable<T> = {
export type InferKeys<O extends Record<keyof any, unknown>, V extends EnumValues<O>> = { export type InferKeys<O extends Record<keyof any, unknown>, V extends EnumValues<O>> = {
[K in keyof O]: O[K] extends V ? K : never; [K in keyof O]: O[K] extends V ? K : never;
}[keyof O]; }[keyof O];
/**
* Type helper that matches any `Function` type. Equivalent to `Function`, but will not raise a warning from Biome.
*/
export type AnyFn = (...args: any[]) => any;
/**
* Type helper to extract non-function properties from a type.
*
* @remarks
* Useful to produce a type that is roughly the same as the type of `{... obj}`, where `obj` is an instance of `T`.
* A couple of differences:
* - Private and protected properties are not included.
* - Nested properties are not recursively extracted. For this, use {@linkcode NonFunctionPropertiesRecursive}
*/
export type NonFunctionProperties<T> = {
[K in keyof T as T[K] extends AnyFn ? never : K]: T[K];
};
/**
* Type helper to extract out non-function properties from a type, recursively applying to nested properties.
*/
export type NonFunctionPropertiesRecursive<Class> = {
[K in keyof Class as Class[K] extends AnyFn ? never : K]: Class[K] extends Array<infer U>
? NonFunctionPropertiesRecursive<U>[]
: Class[K] extends object
? NonFunctionPropertiesRecursive<Class[K]>
: Class[K];
};
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;

View File

@ -50,7 +50,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { EaseType } from "#enums/ease-type"; import { EaseType } from "#enums/ease-type";
import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { ExpGainsSpeed } from "#enums/exp-gains-speed";
import type { ExpNotification } from "#enums/exp-notification"; import { ExpNotification } from "#enums/exp-notification";
import { FormChangeItem } from "#enums/form-change-item"; import { FormChangeItem } from "#enums/form-change-item";
import { GameModes } from "#enums/game-modes"; import { GameModes } from "#enums/game-modes";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
@ -197,6 +197,7 @@ export class BattleScene extends SceneBase {
public enableMoveInfo = true; public enableMoveInfo = true;
public enableRetries = false; public enableRetries = false;
public hideIvs = false; public hideIvs = false;
// TODO: Remove all plain numbers in place of enums or `const object` equivalents for clarity
/** /**
* Determines the condition for a notification should be shown for Candy Upgrades * Determines the condition for a notification should be shown for Candy Upgrades
* - 0 = 'Off' * - 0 = 'Off'
@ -214,7 +215,7 @@ export class BattleScene extends SceneBase {
public uiTheme: UiTheme = UiTheme.DEFAULT; public uiTheme: UiTheme = UiTheme.DEFAULT;
public windowType = 0; public windowType = 0;
public experimentalSprites = false; public experimentalSprites = false;
public musicPreference: number = MusicPreference.ALLGENS; public musicPreference: MusicPreference = MusicPreference.ALLGENS;
public moveAnimations = true; public moveAnimations = true;
public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT; public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT;
public skipSeenDialogues = false; public skipSeenDialogues = false;
@ -225,33 +226,18 @@ export class BattleScene extends SceneBase {
* - 2 = Always (automatically skip animation when hatching 2 or more eggs) * - 2 = Always (automatically skip animation when hatching 2 or more eggs)
*/ */
public eggSkipPreference = 0; public eggSkipPreference = 0;
/** /**
* Defines the experience gain display mode. * Defines the {@linkcode ExpNotification | Experience gain display mode}.
* * @defaultValue {@linkcode ExpNotification.DEFAULT}
* @remarks
* The `expParty` can have several modes:
* - `0` - Default: The normal experience gain display, nothing changed.
* - `1` - Level Up Notification: Displays the level up in the small frame instead of a message.
* - `2` - Skip: No level up frame nor message.
*
* Modes `1` and `2` are still compatible with stats display, level up, new move, etc.
* @default 0 - Uses the default normal experience gain display.
*/ */
public expParty: ExpNotification = 0; public expParty: ExpNotification = ExpNotification.DEFAULT;
public hpBarSpeed = 0; public hpBarSpeed = 0;
public fusionPaletteSwaps = true; public fusionPaletteSwaps = true;
public enableTouchControls = false; public enableTouchControls = false;
public enableVibration = false; public enableVibration = false;
public showBgmBar = true; public showBgmBar = true;
/** Determines the selected battle style. */
/** public battleStyle: BattleStyle = BattleStyle.SWITCH;
* Determines the selected battle style.
* - 0 = 'Switch'
* - 1 = 'Set' - The option to switch the active pokemon at the start of a battle will not display.
*/
public battleStyle: number = BattleStyle.SWITCH;
/** /**
* Defines whether or not to show type effectiveness hints * Defines whether or not to show type effectiveness hints
* - true: No hints * - true: No hints
@ -829,7 +815,8 @@ export class BattleScene extends SceneBase {
/** /**
* Returns an array of Pokemon on both sides of the battle - player first, then enemy. * Returns an array of Pokemon on both sides of the battle - player first, then enemy.
* Does not actually check if the pokemon are on the field or not, and always has length 4 regardless of battle type. * Does not actually check if the pokemon are on the field or not, and always has length 4 regardless of battle type.
* @param activeOnly - Whether to consider only active pokemon; default `false` * @param activeOnly - Whether to consider only active pokemon (as described by {@linkcode Pokemon.isActive()}); default `false`.
* If `true`, will also remove all `null` values from the array.
* @returns An array of {@linkcode Pokemon}, as described above. * @returns An array of {@linkcode Pokemon}, as described above.
*/ */
public getField(activeOnly = false): Pokemon[] { public getField(activeOnly = false): Pokemon[] {
@ -842,9 +829,9 @@ export class BattleScene extends SceneBase {
} }
/** /**
* Used in doubles battles to redirect moves from one pokemon to another when one faints or is removed from the field * Attempt to redirect a move in double battles from a fainted/removed Pokemon to its ally.
* @param removedPokemon {@linkcode Pokemon} the pokemon that is being removed from the field (flee, faint), moves to be redirected FROM * @param removedPokemon - The {@linkcode Pokemon} having been removed from the field.
* @param allyPokemon {@linkcode Pokemon} the pokemon that will have the moves be redirected TO * @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it
*/ */
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
// failsafe: if not a double battle just return // failsafe: if not a double battle just return
@ -870,10 +857,10 @@ export class BattleScene extends SceneBase {
/** /**
* Returns the ModifierBar of this scene, which is declared private and therefore not accessible elsewhere * Returns the ModifierBar of this scene, which is declared private and therefore not accessible elsewhere
* @param isEnemy Whether to return the enemy's modifier bar * @param isEnemy - Whether to return the enemy modifier bar instead of the player bar; default `false`
* @returns {ModifierBar} * @returns The {@linkcode ModifierBar} for the given side of the field
*/ */
getModifierBar(isEnemy?: boolean): ModifierBar { getModifierBar(isEnemy = false): ModifierBar {
return isEnemy ? this.enemyModifierBar : this.modifierBar; return isEnemy ? this.enemyModifierBar : this.modifierBar;
} }
@ -1475,10 +1462,12 @@ export class BattleScene extends SceneBase {
if (!waveIndex && lastBattle) { if (!waveIndex && lastBattle) {
const isNewBiome = this.isNewBiome(lastBattle); const isNewBiome = this.isNewBiome(lastBattle);
/** Whether to reset and recall pokemon */
const resetArenaState = const resetArenaState =
isNewBiome || isNewBiome ||
[BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) ||
this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS; this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS;
for (const enemyPokemon of this.getEnemyParty()) { for (const enemyPokemon of this.getEnemyParty()) {
enemyPokemon.destroy(); enemyPokemon.destroy();
} }
@ -1853,7 +1842,7 @@ export class BattleScene extends SceneBase {
} }
resetSeed(waveIndex?: number): void { resetSeed(waveIndex?: number): void {
const wave = waveIndex || this.currentBattle?.waveIndex || 0; const wave = waveIndex ?? this.currentBattle?.waveIndex ?? 0;
this.waveSeed = shiftCharCodes(this.seed, wave); this.waveSeed = shiftCharCodes(this.seed, wave);
Phaser.Math.RND.sow([this.waveSeed]); Phaser.Math.RND.sow([this.waveSeed]);
console.log("Wave Seed:", this.waveSeed, wave); console.log("Wave Seed:", this.waveSeed, wave);

View File

@ -145,76 +145,141 @@ export class Ability implements Localizable {
return this.attrs.some(attr => attr instanceof targetAttr); return this.attrs.some(attr => attr instanceof targetAttr);
} }
attr<T extends Constructor<AbAttr>>(AttrType: T, ...args: ConstructorParameters<T>): Ability { /**
* Create a new {@linkcode AbAttr} instance and add it to this {@linkcode Ability}.
* @param attrType - The constructor of the {@linkcode AbAttr} to create.
* @param args - The arguments needed to instantiate the given class.
* @returns `this`
*/
attr<T extends Constructor<AbAttr>>(AttrType: T, ...args: ConstructorParameters<T>): this {
const attr = new AttrType(...args); const attr = new AttrType(...args);
this.attrs.push(attr); this.attrs.push(attr);
return this; return this;
} }
/**
* Create a new {@linkcode AbAttr} instance with the given condition and add it to this {@linkcode Ability}.
* Checked before all other conditions, and is unique to the individual {@linkcode AbAttr} being created.
* @param condition - The {@linkcode AbAttrCondition} to add.
* @param attrType - The constructor of the {@linkcode AbAttr} to create.
* @param args - The arguments needed to instantiate the given class.
* @returns `this`
*/
conditionalAttr<T extends Constructor<AbAttr>>( conditionalAttr<T extends Constructor<AbAttr>>(
condition: AbAttrCondition, condition: AbAttrCondition,
AttrType: T, attrType: T,
...args: ConstructorParameters<T> ...args: ConstructorParameters<T>
): Ability { ): this {
const attr = new AttrType(...args); const attr = new attrType(...args);
attr.addCondition(condition); attr.addCondition(condition);
this.attrs.push(attr); this.attrs.push(attr);
return this; return this;
} }
bypassFaint(): Ability { /**
* Make this ability trigger even if the user faints.
* @returns `this`
* @remarks
* This is also required for abilities to trigger when revived via Reviver Seed.
*/
bypassFaint(): this {
this.isBypassFaint = true; this.isBypassFaint = true;
return this; return this;
} }
ignorable(): Ability { /**
* Make this ability ignorable by effects like {@linkcode MoveId.SUNSTEEL_STRIKE | Sunsteel Strike} or {@linkcode AbilityId.MOLD_BREAKER | Mold Breaker}.
* @returns `this`
*/
ignorable(): this {
this.isIgnorable = true; this.isIgnorable = true;
return this; return this;
} }
unsuppressable(): Ability { /**
* Make this ability unsuppressable by effects like {@linkcode MoveId.GASTRO_ACID | Gastro Acid} or {@linkcode AbilityId.NEUTRALIZING_GAS | Neutralizing Gas}.
* @returns `this`
*/
unsuppressable(): this {
this.isSuppressable = false; this.isSuppressable = false;
return this; return this;
} }
uncopiable(): Ability { /**
* Make this ability uncopiable by effects like {@linkcode MoveId.ROLE_PLAY | Role Play} or {@linkcode AbilityId.TRACE | Trace}.
* @returns `this`
*/
uncopiable(): this {
this.isCopiable = false; this.isCopiable = false;
return this; return this;
} }
unreplaceable(): Ability { /**
* Make this ability unreplaceable by effects like {@linkcode MoveId.SIMPLE_BEAM | Simple Beam} or {@linkcode MoveId.ENTRAINMENT | Entrainment}.
* @returns `this`
*/
unreplaceable(): this {
this.isReplaceable = false; this.isReplaceable = false;
return this; return this;
} }
condition(condition: AbAttrCondition): Ability { /**
* Add a condition for this ability to be applied.
* Applies to **all** attributes of the given ability.
* @param condition - The {@linkcode AbAttrCondition} to add
* @returns `this`
* @see {@linkcode AbAttr.canApply} for setting conditions per attribute type
* @see {@linkcode conditionalAttr} for setting individual conditions per attribute instance
* @todo Review if this is necessary anymore - this is used extremely sparingly
*/
condition(condition: AbAttrCondition): this {
this.conditions.push(condition); this.conditions.push(condition);
return this; return this;
} }
/**
* Mark an ability as partially implemented.
* Partial abilities are expected to have some of their core functionality implemented, but may lack
* certain notable features or interactions with other moves or abilities.
* @returns `this`
*/
partial(): this { partial(): this {
this.nameAppend += " (P)"; this.nameAppend += " (P)";
return this; return this;
} }
/**
* Mark an ability as unimplemented.
* Unimplemented abilities are ones which have _none_ of their basic functionality enabled.
* @returns `this`
*/
unimplemented(): this { unimplemented(): this {
this.nameAppend += " (N)"; this.nameAppend += " (N)";
return this; return this;
} }
/** /**
* Internal flag used for developers to document edge cases. When using this, please be sure to document the edge case. * Mark an ability as having one or more edge cases.
* @returns the ability * It may lack certain niche interactions with other moves/abilities, but still functions
* as intended in most cases.
* Does not show up in game and is solely for internal dev use.
*
* When using this, make sure to **document the edge case** (or else this becomes pointless).
* @returns `this`
*/ */
edgeCase(): this { edgeCase(): this {
return this; return this;
} }
} }
/** Base set of parameters passed to every ability attribute's apply method */ /**
* Base set of parameters passed to every ability attribute's {@linkcode AbAttr.apply | apply} method.
*
* Extended by sub-classes to contain additional parameters pertaining to the ability type(s) being triggered.
*/
export interface AbAttrBaseParams { export interface AbAttrBaseParams {
/** The pokemon that has the ability being applied */ /** The pokemon that has the ability being applied */
readonly pokemon: Pokemon; readonly pokemon: Pokemon;
@ -245,9 +310,20 @@ export interface AbAttrParamsWithCancel extends AbAttrBaseParams {
readonly cancelled: BooleanHolder; readonly cancelled: BooleanHolder;
} }
/**
* Abstract class for all ability attributes.
*
* Each {@linkcode Ability} may have any number of individual attributes, each functioning independently from one another.
*/
export abstract class AbAttr { export abstract class AbAttr {
public showAbility: boolean; /**
private extraCondition: AbAttrCondition; * Whether to show this ability as a flyout when applying its effects.
* Should be kept in parity with mainline where possible.
* @defaultValue `true`
*/
public showAbility = true;
/** The additional condition associated with this AbAttr, if any. */
private extraCondition?: AbAttrCondition;
/** /**
* Return whether this attribute is of the given type. * Return whether this attribute is of the given type.
@ -275,21 +351,43 @@ export abstract class AbAttr {
} }
/** /**
* Apply ability effects without checking conditions. * Apply this attribute's effects without checking conditions.
* **Never call this method directly, use {@linkcode applyAbAttrs} instead.** *
* @remarks
* **Never call this method directly!** \
* Use {@linkcode applyAbAttrs} instead.
*/ */
apply(_params: AbAttrBaseParams): void {} apply(_params: AbAttrBaseParams): void {}
// The `Exact` in the next two signatures enforces that the type of the _params operand /**
// is always compatible with the type of apply. This allows fewer fields, but never a type with more. * Return the trigger message to show when this attribute is executed.
* @param _params - The parameters passed to this attribute's {@linkcode apply} function; must match type exactly
* @param _abilityName - The name of the current ability.
* @privateRemarks
* If more fields are provided than needed, any excess can be discarded using destructuring.
* @todo Remove `null` from signature in lieu of using an empty string
*/
getTriggerMessage(_params: Exact<Parameters<this["apply"]>[0]>, _abilityName: string): string | null { getTriggerMessage(_params: Exact<Parameters<this["apply"]>[0]>, _abilityName: string): string | null {
return null; return null;
} }
/**
* Check whether this attribute can have its effects successfully applied.
* Applies to **all** instances of the given attribute.
* @param _params - The parameters passed to this attribute's {@linkcode apply} function; must match type exactly
* @privateRemarks
* If more fields are provided than needed, any excess can be discarded using destructuring.
*/
canApply(_params: Exact<Parameters<this["apply"]>[0]>): boolean { canApply(_params: Exact<Parameters<this["apply"]>[0]>): boolean {
return true; return true;
} }
/**
* Return the additional condition associated with this particular AbAttr instance, if any.
* @returns The extra condition for this {@linkcode AbAttr}, or `null` if none exist
* @todo Make this use `undefined` instead of `null`
* @todo Prevent this from being overridden by sub-classes
*/
getCondition(): AbAttrCondition | null { getCondition(): AbAttrCondition | null {
return this.extraCondition || null; return this.extraCondition || null;
} }
@ -593,7 +691,7 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr {
private immuneType: PokemonType | null; private immuneType: PokemonType | null;
private condition: AbAttrCondition | null; private condition: AbAttrCondition | null;
// TODO: `immuneType` shouldn't be able to be `null` // TODO: Change `NonSuperEffectiveImmunityAbAttr` to not pass `null` as immune type
constructor(immuneType: PokemonType | null, condition?: AbAttrCondition) { constructor(immuneType: PokemonType | null, condition?: AbAttrCondition) {
super(true); super(true);
@ -1526,6 +1624,11 @@ export interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams {
export class FieldMultiplyStatAbAttr extends AbAttr { export class FieldMultiplyStatAbAttr extends AbAttr {
private stat: Stat; private stat: Stat;
private multiplier: number; private multiplier: number;
/**
* Whether this ability can stack with others of the same type for this stat.
* @defaultValue `false`
* @todo Remove due to being literally useless - the ruin abilities are hardcoded to never stack in game
*/
private canStack: boolean; private canStack: boolean;
constructor(stat: Stat, multiplier: number, canStack = false) { constructor(stat: Stat, multiplier: number, canStack = false) {
@ -1546,7 +1649,7 @@ export class FieldMultiplyStatAbAttr extends AbAttr {
} }
/** /**
* applyFieldStat: Tries to multiply a Pokemon's Stat * Atttempt to multiply a Pokemon's Stat.
*/ */
apply({ statVal, hasApplied }: FieldMultiplyStatAbAttrParams): void { apply({ statVal, hasApplied }: FieldMultiplyStatAbAttrParams): void {
statVal.value *= this.multiplier; statVal.value *= this.multiplier;
@ -1572,23 +1675,25 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
} }
/** /**
* Determine if the move type change attribute can be applied * Determine if the move type change attribute can be applied.
* *
* Can be applied if: * Can be applied if:
* - The ability's condition is met, e.g. pixilate only boosts normal moves, * - The ability's condition is met, e.g. pixilate only boosts normal moves,
* - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode MoveId.MULTI_ATTACK} * - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode MoveId.MULTI_ATTACK}
* - The user is not terastallized and using tera blast * - The user is not Terastallized and using Tera Blast
* - The user is not a terastallized terapagos with tera stellar using tera starstorm * - The user is not a Terastallized Terapagos using Stellar-type Tera Starstorm
*/ */
override canApply({ pokemon, opponent: target, move }: MoveTypeChangeAbAttrParams): boolean { override canApply({ pokemon, opponent: target, move }: MoveTypeChangeAbAttrParams): boolean {
return ( return (
(!this.condition || this.condition(pokemon, target, move)) && (!this.condition || this.condition(pokemon, target, move)) &&
!noAbilityTypeOverrideMoves.has(move.id) && !noAbilityTypeOverrideMoves.has(move.id) &&
(!pokemon.isTerastallized || !(
(move.id !== MoveId.TERA_BLAST && pokemon.isTerastallized &&
(move.id !== MoveId.TERA_STARSTORM || (move.id === MoveId.TERA_BLAST ||
pokemon.getTeraType() !== PokemonType.STELLAR || (move.id === MoveId.TERA_STARSTORM &&
!pokemon.hasSpecies(SpeciesId.TERAPAGOS)))) pokemon.getTeraType() === PokemonType.STELLAR &&
pokemon.hasSpecies(SpeciesId.TERAPAGOS)))
)
); );
} }
@ -1661,23 +1766,19 @@ export interface AddSecondStrikeAbAttrParams extends Omit<AugmentMoveInteraction
} }
/** /**
* Class for abilities that convert single-strike moves to two-strike moves (i.e. Parental Bond). * Class for abilities that add additional strikes to single-target moves.
* @param damageMultiplier the damage multiplier for the second strike, relative to the first. * Used by {@linkcode Moves.PARENTAL_BOND | Parental Bond}.
*/ */
export class AddSecondStrikeAbAttr extends PreAttackAbAttr { export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
private damageMultiplier: number;
/** /**
* @param damageMultiplier - The damage multiplier for the second strike, relative to the first * @param damageMultiplier - The damage multiplier for the second strike, relative to the first
*/ */
constructor(damageMultiplier: number) { constructor(private damageMultiplier: number) {
super(false); super(false);
this.damageMultiplier = damageMultiplier;
} }
/** /**
* Return whether the move can be multi-strike enhanced * Return whether the move can be multi-strike enhanced.
*/ */
override canApply({ pokemon, move }: AddSecondStrikeAbAttrParams): boolean { override canApply({ pokemon, move }: AddSecondStrikeAbAttrParams): boolean {
return move.canBeMultiStrikeEnhanced(pokemon, true); return move.canBeMultiStrikeEnhanced(pokemon, true);
@ -2021,9 +2122,10 @@ export abstract class PostAttackAbAttr extends AbAttr {
} }
/** /**
* By default, this method checks that the move used is a damaging attack. * By default, this method checks that the move used is a damaging attack before
* This can be changed by providing a different {@link attackCondition} to the constructor. * applying the effect of any inherited class.
* @see {@link ConfusionOnStatusEffectAbAttr} for an example of an effect that does not require a damaging move. * This can be changed by providing a different {@linkcode attackCondition} to the constructor.
* @see {@linkcode ConfusionOnStatusEffectAbAttr} for an example of an effect that does not require a damaging move.
*/ */
override canApply({ pokemon, opponent, move }: Closed<PostMoveInteractionAbAttrParams>): boolean { override canApply({ pokemon, opponent, move }: Closed<PostMoveInteractionAbAttrParams>): boolean {
return this.attackCondition(pokemon, opponent, move); return this.attackCondition(pokemon, opponent, move);
@ -3511,18 +3613,18 @@ export interface ConfusionOnStatusEffectAbAttrParams extends AbAttrBaseParams {
*/ */
export class ConfusionOnStatusEffectAbAttr extends AbAttr { export class ConfusionOnStatusEffectAbAttr extends AbAttr {
/** List of effects to apply confusion after */ /** List of effects to apply confusion after */
private effects: StatusEffect[]; private effects: ReadonlySet<StatusEffect>;
constructor(...effects: StatusEffect[]) { constructor(...effects: StatusEffect[]) {
super(); super();
this.effects = effects; this.effects = new Set(effects);
} }
/** /**
* @returns Whether the ability can apply confusion to the opponent * @returns Whether the ability can apply confusion to the opponent
*/ */
override canApply({ opponent, effect }: ConfusionOnStatusEffectAbAttrParams): boolean { override canApply({ opponent, effect }: ConfusionOnStatusEffectAbAttrParams): boolean {
return this.effects.includes(effect) && !opponent.isFainted() && opponent.canAddTag(BattlerTagType.CONFUSED); return this.effects.has(effect) && !opponent.isFainted() && opponent.canAddTag(BattlerTagType.CONFUSED);
} }
/** /**
@ -4500,8 +4602,8 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
} }
/** /**
* After the turn ends, resets the status of either the ability holder or their ally * After the turn ends, resets the status of either the user or their ally.
* @param allyTarget Whether to target ally, defaults to false (self-target) * @param allyTarget Whether to target the user's ally; default `false` (self-target)
* *
* @sealed * @sealed
*/ */
@ -4786,17 +4888,22 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
!opp.switchOutStatus, !opp.switchOutStatus,
); );
} }
/** Deals damage to all sleeping opponents equal to 1/8 of their max hp (min 1) */
/** Deal damage to all sleeping, on-field opponents equal to 1/8 of their max hp (min 1). */
override apply({ pokemon, simulated }: AbAttrBaseParams): void { override apply({ pokemon, simulated }: AbAttrBaseParams): void {
if (simulated) { if (simulated) {
return; return;
} }
for (const opp of pokemon.getOpponents()) { for (const opp of pokemon.getOpponents()) {
if ( if ((opp.status?.effect !== StatusEffect.SLEEP && !opp.hasAbility(AbilityId.COMATOSE)) || opp.switchOutStatus) {
(opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(AbilityId.COMATOSE)) && continue;
!opp.hasAbilityWithAttr("BlockNonDirectDamageAbAttr") && }
!opp.switchOutStatus
) { const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, simulated, cancelled });
if (!cancelled.value) {
opp.damageAndUpdate(toDmgValue(opp.getMaxHp() / 8), { result: HitResult.INDIRECT }); opp.damageAndUpdate(toDmgValue(opp.getMaxHp() / 8), { result: HitResult.INDIRECT });
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) }), i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) }),
@ -4809,7 +4916,8 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
/** /**
* Grabs the last failed Pokeball used * Grabs the last failed Pokeball used
* @sealed * @sealed
* @see {@linkcode applyPostTurn} */ * @see {@linkcode applyPostTurn}
*/
export class FetchBallAbAttr extends PostTurnAbAttr { export class FetchBallAbAttr extends PostTurnAbAttr {
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean { override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
return !simulated && !isNullOrUndefined(globalScene.currentBattle.lastUsedPokeball) && !!pokemon.isPlayer; return !simulated && !isNullOrUndefined(globalScene.currentBattle.lastUsedPokeball) && !!pokemon.isPlayer;
@ -5681,11 +5789,13 @@ export class InfiltratorAbAttr extends AbAttr {
* Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable} * Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable}
* moves as if the user had used {@linkcode MoveId.MAGIC_COAT | Magic Coat}. * moves as if the user had used {@linkcode MoveId.MAGIC_COAT | Magic Coat}.
* @sealed * @sealed
* @todo Make reflection a part of this ability's effects
*/ */
export class ReflectStatusMoveAbAttr extends AbAttr { export class ReflectStatusMoveAbAttr extends AbAttr {
private declare readonly _: never; private declare readonly _: never;
} }
// TODO: Make these ability attributes be flags instead of dummy attributes
/** @sealed */ /** @sealed */
export class NoTransformAbilityAbAttr extends AbAttr { export class NoTransformAbilityAbAttr extends AbAttr {
private declare readonly _: never; private declare readonly _: never;

View File

@ -8,23 +8,18 @@ function applySingleAbAttrs<T extends AbAttrString>(
messages: string[] = [], messages: string[] = [],
) { ) {
const { simulated = false, passive = false, pokemon } = params; const { simulated = false, passive = false, pokemon } = params;
if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) { if (!pokemon.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) {
return; return;
} }
const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility();
if ( const attrs = ability.getAttrs(attrType);
gainedMidTurn && if (gainedMidTurn && attrs.some(attr => attr.is("PostSummonAbAttr") && !attr.shouldActivateOnGain())) {
ability.getAttrs(attrType).some(attr => {
attr.is("PostSummonAbAttr") && !attr.shouldActivateOnGain();
})
) {
return; return;
} }
for (const attr of ability.getAttrs(attrType)) { for (const attr of attrs) {
const condition = attr.getCondition(); const condition = attr.getCondition();
let abShown = false;
// We require an `as any` cast to suppress an error about the `params` type not being assignable to // We require an `as any` cast to suppress an error about the `params` type not being assignable to
// the type of the argument expected by `attr.canApply()`. This is OK, because we know that // the type of the argument expected by `attr.canApply()`. This is OK, because we know that
// `attr` is an instance of the `attrType` class provided to the method, and typescript _will_ check // `attr` is an instance of the `attrType` class provided to the method, and typescript _will_ check
@ -33,7 +28,7 @@ function applySingleAbAttrs<T extends AbAttrString>(
continue; continue;
} }
globalScene.phaseManager.setPhaseQueueSplice(); let abShown = false;
if (attr.showAbility && !simulated) { if (attr.showAbility && !simulated) {
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
@ -45,6 +40,7 @@ function applySingleAbAttrs<T extends AbAttrString>(
if (!simulated) { if (!simulated) {
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
} }
// TODO: Should messages be added to the array if they aren't actually shown?
messages.push(message); messages.push(message);
} }
// The `as any` cast here uses the same reasoning as above. // The `as any` cast here uses the same reasoning as above.
@ -57,8 +53,6 @@ function applySingleAbAttrs<T extends AbAttrString>(
if (!simulated) { if (!simulated) {
pokemon.waveData.abilitiesApplied.add(ability.id); pokemon.waveData.abilitiesApplied.add(ability.id);
} }
globalScene.phaseManager.clearPhaseQueueSplice();
} }
} }

View File

@ -19,22 +19,88 @@ import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import type { Arena } from "#field/arena"; import type { Arena } from "#field/arena";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type {
ArenaDelayedAttackTagType,
ArenaScreenTagType,
ArenaTagTypeData,
ArenaTrapTagType,
SerializableArenaTagType,
} from "#types/arena-tags";
import type { Mutable, NonFunctionProperties } from "#types/type-helpers";
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common"; import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
import i18next from "i18next"; import i18next from "i18next";
/*
ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
Examples include (but are not limited to)
- Cross-turn effects that persist even if the user/target switches out, such as Wish, Future Sight, and Happy Hour
- Effects that are applied to a specific side of the field, such as Crafty Shield, Reflect, and Spikes
- Field-Effects, like Gravity and Trick Room
Any arena tag that persists across turns *must* extend from `SerializableArenaTag` in the class definition signature.
Serializable ArenaTags have strict rules for their fields.
These rules ensure that only the data necessary to reconstruct the tag is serialized, and that the
session loader is able to deserialize saved tags correctly.
If the data is static (i.e. it is always the same for all instances of the class, such as the
type that is weakened by Mud Sport/Water Sport), then it must not be defined as a field, and must
instead be defined as a getter.
A static property is also acceptable, though static properties are less ergonomic with inheritance.
If the data is mutable (i.e. it can change over the course of the tag's lifetime), then it *must*
be defined as a field, and it must be set in the `loadTag` method.
Such fields cannot be marked as `private/protected`, as if they were, typescript would omit them from
types that are based off of the class, namely, `ArenaTagTypeData`. It is preferrable to trade the
type-safety of private/protected fields for the type safety when deserializing arena tags from save data.
For data that is mutable only within a turn (e.g. SuppressAbilitiesTag's beingRemoved field),
where it does not make sense to be serialized, the field should use ES2020's private field syntax (a `#` prepended to the field name).
If the field should be accessible outside of the class, then a public getter should be used.
*/
/** Interface containing the serializable fields of ArenaTagData. */
interface BaseArenaTag {
/**
* The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite.
*/
turnCount: number;
/**
* The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move.
*/
sourceMove?: MoveId;
/**
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created the tag, or `undefined` if not set by a Pokemon.
* @todo Implement handling for `ArenaTag`s created by non-pokemon sources (most tags will throw errors without a source)
*/
// Note: Intentionally not using `?`, as the property should always exist, but just be undefined if not present.
sourceId: number | undefined;
/**
* The {@linkcode ArenaTagSide | side of the field} that this arena tag affects.
* @defaultValue `ArenaTagSide.BOTH`
*/
side: ArenaTagSide;
}
/** /**
* An {@linkcode ArenaTag} represents a semi-persistent effect affecting a given side of the field. * An {@linkcode ArenaTag} represents a semi-persistent effect affecting a given _side_ of the field.
* Unlike {@linkcode BattlerTag}s (which are tied to individual {@linkcode Pokemon}), `ArenaTag`s function independently of * Unlike {@linkcode BattlerTag}s (which are tied to individual {@linkcode Pokemon}), `ArenaTag`s function independently of
* the Pokemon currently on-field, only cleared on arena reset or through their respective {@linkcode ArenaTag.lapse | lapse} methods. * the Pokemon currently on-field, only cleared on arena reset or through their respective {@linkcode ArenaTag.lapse | lapse} methods.
*/ */
export abstract class ArenaTag { export abstract class ArenaTag implements BaseArenaTag {
constructor( /** The type of the arena tag */
public tagType: ArenaTagType, public abstract readonly tagType: ArenaTagType;
public turnCount: number, public turnCount: number;
public sourceMove?: MoveId, public sourceMove?: MoveId;
public sourceId?: number, public sourceId: number | undefined;
public side: ArenaTagSide = ArenaTagSide.BOTH, public side: ArenaTagSide;
) {}
constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) {
this.turnCount = turnCount;
this.sourceMove = sourceMove;
this.sourceId = sourceId;
this.side = side;
}
apply(_arena: Arena, _simulated: boolean, ..._args: unknown[]): boolean { apply(_arena: Arena, _simulated: boolean, ..._args: unknown[]): boolean {
return true; return true;
@ -55,8 +121,17 @@ export abstract class ArenaTag {
onOverlap(_arena: Arena, _source: Pokemon | null): void {} onOverlap(_arena: Arena, _source: Pokemon | null): void {}
/**
* Trigger this {@linkcode ArenaTag}'s effect, reducing its duration as applicable.
* Will ignore durations of all tags with durations `<=0`.
* @param _arena - The {@linkcode Arena} at the moment the tag is being lapsed.
* Unused by default but can be used by sub-classes.
* @returns `true` if this tag should be kept; `false` if it should be removed.
*/
lapse(_arena: Arena): boolean { lapse(_arena: Arena): boolean {
return this.turnCount < 1 || !!--this.turnCount; // TODO: Rather than treating negative duration tags as being indefinite,
// make all duration based classes inherit from their own sub-class
return this.turnCount < 1 || --this.turnCount > 0;
} }
getMoveName(): string | null { getMoveName(): string | null {
@ -66,9 +141,9 @@ export abstract class ArenaTag {
/** /**
* When given a arena tag or json representing one, load the data for it. * When given a arena tag or json representing one, load the data for it.
* This is meant to be inherited from by any arena tag with custom attributes * This is meant to be inherited from by any arena tag with custom attributes
* @param {ArenaTag | any} source An arena tag * @param source - The {@linkcode BaseArenaTag} being loaded
*/ */
loadTag(source: ArenaTag | any): void { loadTag(source: BaseArenaTag): void {
this.turnCount = source.turnCount; this.turnCount = source.turnCount;
this.sourceMove = source.sourceMove; this.sourceMove = source.sourceMove;
this.sourceId = source.sourceId; this.sourceId = source.sourceId;
@ -101,13 +176,21 @@ export abstract class ArenaTag {
} }
} }
/**
* Abstract class for arena tags that can persist across turns.
*/
export abstract class SerializableArenaTag extends ArenaTag {
abstract readonly tagType: SerializableArenaTagType;
}
/** /**
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Mist_(move) Mist}. * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Mist_(move) Mist}.
* Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist. * Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist.
*/ */
export class MistTag extends ArenaTag { export class MistTag extends SerializableArenaTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { readonly tagType = ArenaTagType.MIST;
super(ArenaTagType.MIST, turnCount, MoveId.MIST, sourceId, side); constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.MIST, sourceId, side);
} }
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
@ -164,33 +247,11 @@ export class MistTag extends ArenaTag {
/** /**
* Reduces the damage of specific move categories in the arena. * Reduces the damage of specific move categories in the arena.
* @extends ArenaTag
*/ */
export class WeakenMoveScreenTag extends ArenaTag { export abstract class WeakenMoveScreenTag extends SerializableArenaTag {
protected weakenedCategories: MoveCategory[]; public abstract readonly tagType: ArenaScreenTagType;
// Getter to avoid unnecessary serialization and prevent modification
/** protected abstract get weakenedCategories(): MoveCategory[];
* Creates a new instance of the WeakenMoveScreenTag class.
*
* @param tagType - The type of the arena tag.
* @param turnCount - The number of turns the tag is active.
* @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 weakenedCategories - The categories of moves that are weakened by this tag.
*/
constructor(
tagType: ArenaTagType,
turnCount: number,
sourceMove: MoveId,
sourceId: number,
side: ArenaTagSide,
weakenedCategories: MoveCategory[],
) {
super(tagType, turnCount, sourceMove, sourceId, side);
this.weakenedCategories = weakenedCategories;
}
/** /**
* Applies the weakening effect to the move. * Applies the weakening effect to the move.
@ -227,8 +288,13 @@ export class WeakenMoveScreenTag extends ArenaTag {
* Used by {@linkcode MoveId.REFLECT} * Used by {@linkcode MoveId.REFLECT}
*/ */
class ReflectTag extends WeakenMoveScreenTag { class ReflectTag extends WeakenMoveScreenTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.REFLECT;
super(ArenaTagType.REFLECT, turnCount, MoveId.REFLECT, sourceId, side, [MoveCategory.PHYSICAL]); protected get weakenedCategories(): [MoveCategory.PHYSICAL] {
return [MoveCategory.PHYSICAL];
}
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.REFLECT, sourceId, side);
} }
onAdd(_arena: Arena, quiet = false): void { onAdd(_arena: Arena, quiet = false): void {
@ -247,8 +313,12 @@ class ReflectTag extends WeakenMoveScreenTag {
* Used by {@linkcode MoveId.LIGHT_SCREEN} * Used by {@linkcode MoveId.LIGHT_SCREEN}
*/ */
class LightScreenTag extends WeakenMoveScreenTag { class LightScreenTag extends WeakenMoveScreenTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.LIGHT_SCREEN;
super(ArenaTagType.LIGHT_SCREEN, turnCount, MoveId.LIGHT_SCREEN, sourceId, side, [MoveCategory.SPECIAL]); protected get weakenedCategories(): [MoveCategory.SPECIAL] {
return [MoveCategory.SPECIAL];
}
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.LIGHT_SCREEN, sourceId, side);
} }
onAdd(_arena: Arena, quiet = false): void { onAdd(_arena: Arena, quiet = false): void {
@ -267,11 +337,13 @@ class LightScreenTag extends WeakenMoveScreenTag {
* Used by {@linkcode MoveId.AURORA_VEIL} * Used by {@linkcode MoveId.AURORA_VEIL}
*/ */
class AuroraVeilTag extends WeakenMoveScreenTag { class AuroraVeilTag extends WeakenMoveScreenTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.AURORA_VEIL;
super(ArenaTagType.AURORA_VEIL, turnCount, MoveId.AURORA_VEIL, sourceId, side, [ protected get weakenedCategories(): [MoveCategory.PHYSICAL, MoveCategory.SPECIAL] {
MoveCategory.SPECIAL, return [MoveCategory.PHYSICAL, MoveCategory.SPECIAL];
MoveCategory.PHYSICAL, }
]);
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.AURORA_VEIL, sourceId, side);
} }
onAdd(_arena: Arena, quiet = false): void { onAdd(_arena: Arena, quiet = false): void {
@ -291,21 +363,23 @@ type ProtectConditionFunc = (arena: Arena, moveId: MoveId) => boolean;
* Class to implement conditional team protection * Class to implement conditional team protection
* applies protection based on the attributes of incoming moves * applies protection based on the attributes of incoming moves
*/ */
export class ConditionalProtectTag extends ArenaTag { export abstract class ConditionalProtectTag extends ArenaTag {
/** The condition function to determine which moves are negated */ /** The condition function to determine which moves are negated */
protected protectConditionFunc: ProtectConditionFunc; protected protectConditionFunc: ProtectConditionFunc;
/** Does this apply to all moves, including those that ignore other forms of protection? */ /**
* Whether this protection effect should apply to _all_ moves, including ones that ignore other forms of protection.
* @defaultValue `false`
*/
protected ignoresBypass: boolean; protected ignoresBypass: boolean;
constructor( constructor(
tagType: ArenaTagType,
sourceMove: MoveId, sourceMove: MoveId,
sourceId: number, sourceId: number | undefined,
side: ArenaTagSide, side: ArenaTagSide,
condition: ProtectConditionFunc, condition: ProtectConditionFunc,
ignoresBypass = false, ignoresBypass = false,
) { ) {
super(tagType, 1, sourceMove, sourceId, side); super(1, sourceMove, sourceId, side);
this.protectConditionFunc = condition; this.protectConditionFunc = condition;
this.ignoresBypass = ignoresBypass; this.ignoresBypass = ignoresBypass;
@ -391,8 +465,9 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (_arena, moveId) => {
* Condition: The incoming move has increased priority. * Condition: The incoming move has increased priority.
*/ */
class QuickGuardTag extends ConditionalProtectTag { class QuickGuardTag extends ConditionalProtectTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.QUICK_GUARD;
super(ArenaTagType.QUICK_GUARD, MoveId.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc);
} }
} }
@ -422,8 +497,9 @@ const WideGuardConditionFunc: ProtectConditionFunc = (_arena, moveId): boolean =
* can be an ally or enemy. * can be an ally or enemy.
*/ */
class WideGuardTag extends ConditionalProtectTag { class WideGuardTag extends ConditionalProtectTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.WIDE_GUARD;
super(ArenaTagType.WIDE_GUARD, MoveId.WIDE_GUARD, sourceId, side, WideGuardConditionFunc); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.WIDE_GUARD, sourceId, side, WideGuardConditionFunc);
} }
} }
@ -444,8 +520,9 @@ const MatBlockConditionFunc: ProtectConditionFunc = (_arena, moveId): boolean =>
* Condition: The incoming move is a Physical or Special attack move. * Condition: The incoming move is a Physical or Special attack move.
*/ */
class MatBlockTag extends ConditionalProtectTag { class MatBlockTag extends ConditionalProtectTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.MAT_BLOCK;
super(ArenaTagType.MAT_BLOCK, MoveId.MAT_BLOCK, sourceId, side, MatBlockConditionFunc); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.MAT_BLOCK, sourceId, side, MatBlockConditionFunc);
} }
onAdd(_arena: Arena) { onAdd(_arena: Arena) {
@ -488,8 +565,9 @@ const CraftyShieldConditionFunc: ProtectConditionFunc = (_arena, moveId) => {
* not target all Pokemon or sides of the field. * not target all Pokemon or sides of the field.
*/ */
class CraftyShieldTag extends ConditionalProtectTag { class CraftyShieldTag extends ConditionalProtectTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.CRAFTY_SHIELD;
super(ArenaTagType.CRAFTY_SHIELD, MoveId.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true);
} }
} }
@ -497,17 +575,8 @@ class CraftyShieldTag extends ConditionalProtectTag {
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Lucky_Chant_(move) Lucky Chant}. * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Lucky_Chant_(move) Lucky Chant}.
* Prevents critical hits against the tag's side. * Prevents critical hits against the tag's side.
*/ */
export class NoCritTag extends ArenaTag { export class NoCritTag extends SerializableArenaTag {
/** public readonly tagType = ArenaTagType.NO_CRIT;
* Constructor method for the NoCritTag class
* @param turnCount `number` the number of turns this effect lasts
* @param sourceMove {@linkcode MoveId} the move that created this effect
* @param sourceId `number` the ID of the {@linkcode Pokemon} that created this effect
* @param side {@linkcode ArenaTagSide} the side to which this effect belongs
*/
constructor(turnCount: number, sourceMove: MoveId, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.NO_CRIT, turnCount, sourceMove, sourceId, side);
}
/** Queues a message upon adding this effect to the field */ /** Queues a message upon adding this effect to the field */
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
@ -538,23 +607,9 @@ export class NoCritTag extends ArenaTag {
/** /**
* Abstract class to implement weakened moves of a specific type. * Abstract class to implement weakened moves of a specific type.
*/ */
export class WeakenMoveTypeTag extends ArenaTag { export abstract class WeakenMoveTypeTag extends SerializableArenaTag {
private weakenedType: PokemonType; abstract readonly tagType: ArenaTagType.MUD_SPORT | ArenaTagType.WATER_SPORT;
abstract get weakenedType(): PokemonType;
/**
* Creates a new instance of the WeakenMoveTypeTag class.
*
* @param tagType - The type of the arena tag.
* @param turnCount - The number of turns the tag is active.
* @param type - The type being weakened from this tag.
* @param sourceMove - The move that created the tag.
* @param sourceId - The ID of the source of the tag.
*/
constructor(tagType: ArenaTagType, turnCount: number, type: PokemonType, sourceMove: MoveId, sourceId: number) {
super(tagType, turnCount, sourceMove, sourceId);
this.weakenedType = type;
}
/** /**
* Reduces an attack's power by 0.33x if it matches this tag's weakened type. * Reduces an attack's power by 0.33x if it matches this tag's weakened type.
@ -578,8 +633,12 @@ export class WeakenMoveTypeTag extends ArenaTag {
* Weakens Electric type moves for a set amount of turns, usually 5. * Weakens Electric type moves for a set amount of turns, usually 5.
*/ */
class MudSportTag extends WeakenMoveTypeTag { class MudSportTag extends WeakenMoveTypeTag {
constructor(turnCount: number, sourceId: number) { public readonly tagType = ArenaTagType.MUD_SPORT;
super(ArenaTagType.MUD_SPORT, turnCount, PokemonType.ELECTRIC, MoveId.MUD_SPORT, sourceId); override get weakenedType(): PokemonType.ELECTRIC {
return PokemonType.ELECTRIC;
}
constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.MUD_SPORT, sourceId);
} }
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
@ -596,8 +655,12 @@ class MudSportTag extends WeakenMoveTypeTag {
* Weakens Fire type moves for a set amount of turns, usually 5. * Weakens Fire type moves for a set amount of turns, usually 5.
*/ */
class WaterSportTag extends WeakenMoveTypeTag { class WaterSportTag extends WeakenMoveTypeTag {
constructor(turnCount: number, sourceId: number) { public readonly tagType = ArenaTagType.WATER_SPORT;
super(ArenaTagType.WATER_SPORT, turnCount, PokemonType.FIRE, MoveId.WATER_SPORT, sourceId); override get weakenedType(): PokemonType.FIRE {
return PokemonType.FIRE;
}
constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.WATER_SPORT, sourceId);
} }
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
@ -615,8 +678,9 @@ class WaterSportTag extends WeakenMoveTypeTag {
* Converts Normal-type moves to Electric type for the rest of the turn. * Converts Normal-type moves to Electric type for the rest of the turn.
*/ */
export class IonDelugeTag extends ArenaTag { export class IonDelugeTag extends ArenaTag {
public readonly tagType = ArenaTagType.ION_DELUGE;
constructor(sourceMove?: MoveId) { constructor(sourceMove?: MoveId) {
super(ArenaTagType.ION_DELUGE, 1, sourceMove); super(1, sourceMove);
} }
/** Queues an on-add message */ /** Queues an on-add message */
@ -645,7 +709,8 @@ export class IonDelugeTag extends ArenaTag {
/** /**
* Abstract class to implement arena traps. * Abstract class to implement arena traps.
*/ */
export class ArenaTrapTag extends ArenaTag { export abstract class ArenaTrapTag extends SerializableArenaTag {
abstract readonly tagType: ArenaTrapTagType;
public layers: number; public layers: number;
public maxLayers: number; public maxLayers: number;
@ -658,8 +723,8 @@ export class ArenaTrapTag extends ArenaTag {
* @param side - The side (player or enemy) the tag affects. * @param side - The side (player or enemy) the tag affects.
* @param maxLayers - The maximum amount of layers this tag can have. * @param maxLayers - The maximum amount of layers this tag can have.
*/ */
constructor(tagType: ArenaTagType, sourceMove: MoveId, sourceId: number, side: ArenaTagSide, maxLayers: number) { constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide, maxLayers: number) {
super(tagType, 0, sourceMove, sourceId, side); super(0, sourceMove, sourceId, side);
this.layers = 1; this.layers = 1;
this.maxLayers = maxLayers; this.maxLayers = maxLayers;
@ -698,7 +763,7 @@ export class ArenaTrapTag extends ArenaTag {
: Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2); : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
} }
loadTag(source: any): void { loadTag(source: NonFunctionProperties<ArenaTrapTag>): void {
super.loadTag(source); super.loadTag(source);
this.layers = source.layers; this.layers = source.layers;
this.maxLayers = source.maxLayers; this.maxLayers = source.maxLayers;
@ -711,8 +776,9 @@ export class ArenaTrapTag extends ArenaTag {
* in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap. * in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap.
*/ */
class SpikesTag extends ArenaTrapTag { class SpikesTag extends ArenaTrapTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.SPIKES;
super(ArenaTagType.SPIKES, MoveId.SPIKES, sourceId, side, 3); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.SPIKES, sourceId, side, 3);
} }
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
@ -769,11 +835,12 @@ class SpikesTag extends ArenaTrapTag {
* Pokémon summoned into this trap remove it entirely. * Pokémon summoned into this trap remove it entirely.
*/ */
class ToxicSpikesTag extends ArenaTrapTag { class ToxicSpikesTag extends ArenaTrapTag {
private neutralized: boolean; #neutralized: boolean;
public readonly tagType = ArenaTagType.TOXIC_SPIKES;
constructor(sourceId: number, side: ArenaTagSide) { constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(ArenaTagType.TOXIC_SPIKES, MoveId.TOXIC_SPIKES, sourceId, side, 2); super(MoveId.TOXIC_SPIKES, sourceId, side, 2);
this.neutralized = false; this.#neutralized = false;
} }
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
@ -799,7 +866,7 @@ class ToxicSpikesTag extends ArenaTrapTag {
} }
onRemove(arena: Arena): void { onRemove(arena: Arena): void {
if (!this.neutralized) { if (!this.#neutralized) {
super.onRemove(arena); super.onRemove(arena);
} }
} }
@ -810,7 +877,7 @@ class ToxicSpikesTag extends ArenaTrapTag {
return true; return true;
} }
if (pokemon.isOfType(PokemonType.POISON)) { if (pokemon.isOfType(PokemonType.POISON)) {
this.neutralized = true; this.#neutralized = true;
if (globalScene.arena.removeTag(this.tagType)) { if (globalScene.arena.removeTag(this.tagType)) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
@ -850,8 +917,9 @@ class ToxicSpikesTag extends ArenaTrapTag {
* who is summoned into the trap, based on the Rock type's type effectiveness. * who is summoned into the trap, based on the Rock type's type effectiveness.
*/ */
class StealthRockTag extends ArenaTrapTag { class StealthRockTag extends ArenaTrapTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.STEALTH_ROCK;
super(ArenaTagType.STEALTH_ROCK, MoveId.STEALTH_ROCK, sourceId, side, 1); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.STEALTH_ROCK, sourceId, side, 1);
} }
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
@ -939,8 +1007,9 @@ class StealthRockTag extends ArenaTrapTag {
* to any Pokémon who is summoned into this trap. * to any Pokémon who is summoned into this trap.
*/ */
class StickyWebTag extends ArenaTrapTag { class StickyWebTag extends ArenaTrapTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.STICKY_WEB;
super(ArenaTagType.STICKY_WEB, MoveId.STICKY_WEB, sourceId, side, 1); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.STICKY_WEB, sourceId, side, 1);
} }
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
@ -1007,14 +1076,57 @@ class StickyWebTag extends ArenaTrapTag {
} }
} }
/**
* Arena Tag class for delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}.
* Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used),
* and deals damage after the turn count is reached.
*/
export class DelayedAttackTag extends SerializableArenaTag {
public targetIndex: BattlerIndex;
public readonly tagType: ArenaDelayedAttackTagType;
constructor(
tagType: ArenaTagType.DOOM_DESIRE | ArenaTagType.FUTURE_SIGHT,
sourceMove: MoveId | undefined,
sourceId: number | undefined,
targetIndex: BattlerIndex,
side: ArenaTagSide = ArenaTagSide.BOTH,
) {
super(3, sourceMove, sourceId, side);
this.tagType = tagType;
this.targetIndex = targetIndex;
this.side = side;
}
lapse(arena: Arena): boolean {
const ret = super.lapse(arena);
if (!ret) {
// TODO: This should not add to move history (for Spite)
globalScene.phaseManager.unshiftNew(
"MoveEffectPhase",
this.sourceId!,
[this.targetIndex],
allMoves[this.sourceMove!],
MoveUseMode.FOLLOW_UP,
); // TODO: are those bangs correct?
}
return ret;
}
onRemove(_arena: Arena): void {}
}
/** /**
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Trick_Room_(move) Trick Room}. * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Trick_Room_(move) Trick Room}.
* Reverses the Speed stats for all Pokémon on the field as long as this arena tag is up, * Reverses the Speed stats for all Pokémon on the field as long as this arena tag is up,
* also reversing the turn order for all Pokémon on the field as well. * also reversing the turn order for all Pokémon on the field as well.
*/ */
export class TrickRoomTag extends ArenaTag { export class TrickRoomTag extends SerializableArenaTag {
constructor(turnCount: number, sourceId: number) { public readonly tagType = ArenaTagType.TRICK_ROOM;
super(ArenaTagType.TRICK_ROOM, turnCount, MoveId.TRICK_ROOM, sourceId); constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.TRICK_ROOM, sourceId);
} }
/** /**
@ -1056,9 +1168,10 @@ export class TrickRoomTag extends ArenaTag {
* Grounds all Pokémon on the field, including Flying-types and those with * Grounds all Pokémon on the field, including Flying-types and those with
* {@linkcode AbilityId.LEVITATE} for the duration of the arena tag, usually 5 turns. * {@linkcode AbilityId.LEVITATE} for the duration of the arena tag, usually 5 turns.
*/ */
export class GravityTag extends ArenaTag { export class GravityTag extends SerializableArenaTag {
constructor(turnCount: number) { public readonly tagType = ArenaTagType.GRAVITY;
super(ArenaTagType.GRAVITY, turnCount, MoveId.GRAVITY); constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.GRAVITY, sourceId);
} }
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
@ -1084,9 +1197,10 @@ export class GravityTag extends ArenaTag {
* Doubles the Speed of the Pokémon who created this arena tag, as well as all allied Pokémon. * Doubles the Speed of the Pokémon who created this arena tag, as well as all allied Pokémon.
* Applies this arena tag for 4 turns (including the turn the move was used). * Applies this arena tag for 4 turns (including the turn the move was used).
*/ */
class TailwindTag extends ArenaTag { class TailwindTag extends SerializableArenaTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.TAILWIND;
super(ArenaTagType.TAILWIND, turnCount, MoveId.TAILWIND, sourceId, side); constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.TAILWIND, sourceId, side);
} }
onAdd(_arena: Arena, quiet = false): void { onAdd(_arena: Arena, quiet = false): void {
@ -1152,9 +1266,10 @@ class TailwindTag extends ArenaTag {
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Happy_Hour_(move) Happy Hour}. * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Happy_Hour_(move) Happy Hour}.
* Doubles the prize money from trainers and money moves like {@linkcode MoveId.PAY_DAY} and {@linkcode MoveId.MAKE_IT_RAIN}. * Doubles the prize money from trainers and money moves like {@linkcode MoveId.PAY_DAY} and {@linkcode MoveId.MAKE_IT_RAIN}.
*/ */
class HappyHourTag extends ArenaTag { class HappyHourTag extends SerializableArenaTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.HAPPY_HOUR;
super(ArenaTagType.HAPPY_HOUR, turnCount, MoveId.HAPPY_HOUR, sourceId, side); constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.HAPPY_HOUR, sourceId, side);
} }
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
@ -1167,8 +1282,9 @@ class HappyHourTag extends ArenaTag {
} }
class SafeguardTag extends ArenaTag { class SafeguardTag extends ArenaTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.SAFEGUARD;
super(ArenaTagType.SAFEGUARD, turnCount, MoveId.SAFEGUARD, sourceId, side); constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.SAFEGUARD, sourceId, side);
} }
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
@ -1189,18 +1305,21 @@ class SafeguardTag extends ArenaTag {
} }
class NoneTag extends ArenaTag { class NoneTag extends ArenaTag {
public readonly tagType = ArenaTagType.NONE;
constructor() { constructor() {
super(ArenaTagType.NONE, 0); super(0);
} }
} }
/** /**
* This arena tag facilitates the application of the move Imprison * 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 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. * Imprison will apply to any opposing Pokemon that switch onto the field as well.
*/ */
class ImprisonTag extends ArenaTrapTag { class ImprisonTag extends ArenaTrapTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.IMPRISON;
super(ArenaTagType.IMPRISON, MoveId.IMPRISON, sourceId, side, 1); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.IMPRISON, sourceId, side, 1);
} }
/** /**
@ -1255,7 +1374,9 @@ class ImprisonTag extends ArenaTrapTag {
*/ */
override onRemove(): void { override onRemove(): void {
const party = this.getAffectedPokemon(); const party = this.getAffectedPokemon();
party.forEach(p => p.removeTag(BattlerTagType.IMPRISON)); party.forEach(p => {
p.removeTag(BattlerTagType.IMPRISON);
});
} }
} }
@ -1266,9 +1387,10 @@ class ImprisonTag extends ArenaTrapTag {
* Damages all non-Fire-type Pokemon on the given side of the field at the end * Damages all non-Fire-type Pokemon on the given side of the field at the end
* of each turn for 4 turns. * of each turn for 4 turns.
*/ */
class FireGrassPledgeTag extends ArenaTag { class FireGrassPledgeTag extends SerializableArenaTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.FIRE_GRASS_PLEDGE;
super(ArenaTagType.FIRE_GRASS_PLEDGE, 4, MoveId.FIRE_PLEDGE, sourceId, side); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(4, MoveId.FIRE_PLEDGE, sourceId, side);
} }
override onAdd(_arena: Arena): void { override onAdd(_arena: Arena): void {
@ -1314,9 +1436,10 @@ class FireGrassPledgeTag extends ArenaTag {
* Doubles the secondary effect chance of moves from Pokemon on the * Doubles the secondary effect chance of moves from Pokemon on the
* given side of the field for 4 turns. * given side of the field for 4 turns.
*/ */
class WaterFirePledgeTag extends ArenaTag { class WaterFirePledgeTag extends SerializableArenaTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.WATER_FIRE_PLEDGE;
super(ArenaTagType.WATER_FIRE_PLEDGE, 4, MoveId.WATER_PLEDGE, sourceId, side); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(4, MoveId.WATER_PLEDGE, sourceId, side);
} }
override onAdd(_arena: Arena): void { override onAdd(_arena: Arena): void {
@ -1348,9 +1471,10 @@ class WaterFirePledgeTag extends ArenaTag {
* and {@link https://bulbapedia.bulbagarden.net/wiki/Water_Pledge_(move) | Water Pledge}. * and {@link https://bulbapedia.bulbagarden.net/wiki/Water_Pledge_(move) | Water Pledge}.
* Quarters the Speed of Pokemon on the given side of the field for 4 turns. * Quarters the Speed of Pokemon on the given side of the field for 4 turns.
*/ */
class GrassWaterPledgeTag extends ArenaTag { class GrassWaterPledgeTag extends SerializableArenaTag {
constructor(sourceId: number, side: ArenaTagSide) { public readonly tagType = ArenaTagType.GRASS_WATER_PLEDGE;
super(ArenaTagType.GRASS_WATER_PLEDGE, 4, MoveId.GRASS_PLEDGE, sourceId, side); constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(4, MoveId.GRASS_PLEDGE, sourceId, side);
} }
override onAdd(_arena: Arena): void { override onAdd(_arena: Arena): void {
@ -1370,9 +1494,10 @@ class GrassWaterPledgeTag extends ArenaTag {
* If a Pokémon that's on the field when Fairy Lock is used goes on to faint later in the same turn, * If a Pokémon that's on the field when Fairy Lock is used goes on to faint later in the same turn,
* the Pokémon that replaces it will still be unable to switch out in the following turn. * the Pokémon that replaces it will still be unable to switch out in the following turn.
*/ */
export class FairyLockTag extends ArenaTag { export class FairyLockTag extends SerializableArenaTag {
constructor(turnCount: number, sourceId: number) { public readonly tagType = ArenaTagType.FAIRY_LOCK;
super(ArenaTagType.FAIRY_LOCK, turnCount, MoveId.FAIRY_LOCK, sourceId); constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.FAIRY_LOCK, sourceId);
} }
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
@ -1386,15 +1511,29 @@ export class FairyLockTag extends ArenaTag {
* Keeps track of the number of pokemon on the field with Neutralizing Gas - If it drops to zero, the effect is ended and abilities are reactivated * Keeps track of the number of pokemon on the field with Neutralizing Gas - If it drops to zero, the effect is ended and abilities are reactivated
* *
* Additionally ends onLose abilities when it is activated * Additionally ends onLose abilities when it is activated
* @sealed
*/ */
export class SuppressAbilitiesTag extends ArenaTag { export class SuppressAbilitiesTag extends SerializableArenaTag {
private sourceCount: number; // Source count is allowed to be inwardly mutable, but outwardly immutable
private beingRemoved: boolean; public readonly sourceCount: number;
public readonly tagType = ArenaTagType.NEUTRALIZING_GAS;
// Private field prevents field from appearing during serialization
/** Whether the tag is in the process of being removed */
#beingRemoved: boolean;
/** Whether the tag is in the process of being removed */
public get beingRemoved(): boolean {
return this.#beingRemoved;
}
constructor(sourceId: number) { constructor(sourceId?: number) {
super(ArenaTagType.NEUTRALIZING_GAS, 0, undefined, sourceId); super(0, undefined, sourceId);
this.sourceCount = 1; this.sourceCount = 1;
this.beingRemoved = false; this.#beingRemoved = false;
}
public override loadTag(source: NonFunctionProperties<SuppressAbilitiesTag>): void {
super.loadTag(source);
(this as Mutable<this>).sourceCount = source.sourceCount;
} }
public override onAdd(_arena: Arena): void { public override onAdd(_arena: Arena): void {
@ -1406,19 +1545,21 @@ export class SuppressAbilitiesTag extends ArenaTag {
if (fieldPokemon && fieldPokemon.id !== pokemon.id) { if (fieldPokemon && fieldPokemon.id !== pokemon.id) {
// TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing // TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing
// the appropriate attributes (preLEaveField and IllusionBreak) // the appropriate attributes (preLEaveField and IllusionBreak)
[true, false].forEach(passive => applyOnLoseAbAttrs({ pokemon: fieldPokemon, passive })); [true, false].forEach(passive => {
applyOnLoseAbAttrs({ pokemon: fieldPokemon, passive });
});
} }
} }
} }
} }
public override onOverlap(_arena: Arena, source: Pokemon | null): void { public override onOverlap(_arena: Arena, source: Pokemon | null): void {
this.sourceCount++; (this as Mutable<this>).sourceCount++;
this.playActivationMessage(source); this.playActivationMessage(source);
} }
public onSourceLeave(arena: Arena): void { public onSourceLeave(arena: Arena): void {
this.sourceCount--; (this as Mutable<this>).sourceCount--;
if (this.sourceCount <= 0) { if (this.sourceCount <= 0) {
arena.removeTag(ArenaTagType.NEUTRALIZING_GAS); arena.removeTag(ArenaTagType.NEUTRALIZING_GAS);
} else if (this.sourceCount === 1) { } else if (this.sourceCount === 1) {
@ -1436,7 +1577,7 @@ export class SuppressAbilitiesTag extends ArenaTag {
} }
public override onRemove(_arena: Arena, quiet = false) { public override onRemove(_arena: Arena, quiet = false) {
this.beingRemoved = true; this.#beingRemoved = true;
if (!quiet) { if (!quiet) {
globalScene.phaseManager.queueMessage(i18next.t("arenaTag:neutralizingGasOnRemove")); globalScene.phaseManager.queueMessage(i18next.t("arenaTag:neutralizingGasOnRemove"));
} }
@ -1444,7 +1585,9 @@ export class SuppressAbilitiesTag extends ArenaTag {
for (const pokemon of globalScene.getField(true)) { for (const pokemon of globalScene.getField(true)) {
// There is only one pokemon with this attr on the field on removal, so its abilities are already active // There is only one pokemon with this attr on the field on removal, so its abilities are already active
if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) { if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) {
[true, false].forEach(passive => applyOnGainAbAttrs({ pokemon, passive })); [true, false].forEach(passive => {
applyOnGainAbAttrs({ pokemon, passive });
});
} }
} }
} }
@ -1453,10 +1596,6 @@ export class SuppressAbilitiesTag extends ArenaTag {
return this.sourceCount > 1; return this.sourceCount > 1;
} }
public isBeingRemoved() {
return this.beingRemoved;
}
private playActivationMessage(pokemon: Pokemon | null) { private playActivationMessage(pokemon: Pokemon | null) {
if (pokemon) { if (pokemon) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
@ -1473,7 +1612,7 @@ export function getArenaTag(
tagType: ArenaTagType, tagType: ArenaTagType,
turnCount: number, turnCount: number,
sourceMove: MoveId | undefined, sourceMove: MoveId | undefined,
sourceId: number, sourceId: number | undefined,
side: ArenaTagSide = ArenaTagSide.BOTH, side: ArenaTagSide = ArenaTagSide.BOTH,
): ArenaTag | null { ): ArenaTag | null {
switch (tagType) { switch (tagType) {
@ -1488,7 +1627,7 @@ export function getArenaTag(
case ArenaTagType.CRAFTY_SHIELD: case ArenaTagType.CRAFTY_SHIELD:
return new CraftyShieldTag(sourceId, side); return new CraftyShieldTag(sourceId, side);
case ArenaTagType.NO_CRIT: case ArenaTagType.NO_CRIT:
return new NoCritTag(turnCount, sourceMove!, sourceId, side); // TODO: is this bang correct? return new NoCritTag(turnCount, sourceMove, sourceId, side);
case ArenaTagType.MUD_SPORT: case ArenaTagType.MUD_SPORT:
return new MudSportTag(turnCount, sourceId); return new MudSportTag(turnCount, sourceId);
case ArenaTagType.WATER_SPORT: case ArenaTagType.WATER_SPORT:
@ -1506,7 +1645,7 @@ export function getArenaTag(
case ArenaTagType.TRICK_ROOM: case ArenaTagType.TRICK_ROOM:
return new TrickRoomTag(turnCount, sourceId); return new TrickRoomTag(turnCount, sourceId);
case ArenaTagType.GRAVITY: case ArenaTagType.GRAVITY:
return new GravityTag(turnCount); return new GravityTag(turnCount, sourceId);
case ArenaTagType.REFLECT: case ArenaTagType.REFLECT:
return new ReflectTag(turnCount, sourceId, side); return new ReflectTag(turnCount, sourceId, side);
case ArenaTagType.LIGHT_SCREEN: case ArenaTagType.LIGHT_SCREEN:
@ -1538,12 +1677,43 @@ export function getArenaTag(
/** /**
* When given a battler tag or json representing one, creates an actual ArenaTag object with the same data. * When given a battler tag or json representing one, creates an actual ArenaTag object with the same data.
* @param {ArenaTag | any} source An arena tag * @param source - An arena tag
* @return {ArenaTag} The valid arena tag * @returns The valid arena tag
*/ */
export function loadArenaTag(source: ArenaTag | any): ArenaTag { export function loadArenaTag(source: ArenaTag | ArenaTagTypeData): ArenaTag {
const tag = const tag =
getArenaTag(source.tagType, source.sourceId, source.sourceMove, source.turnCount, source.side) ?? new NoneTag(); getArenaTag(source.tagType, source.sourceId, source.sourceMove, source.turnCount, source.side) ?? new NoneTag();
tag.loadTag(source); tag.loadTag(source);
return tag; return tag;
} }
export type ArenaTagTypeMap = {
[ArenaTagType.MUD_SPORT]: MudSportTag;
[ArenaTagType.WATER_SPORT]: WaterSportTag;
[ArenaTagType.ION_DELUGE]: IonDelugeTag;
[ArenaTagType.SPIKES]: SpikesTag;
[ArenaTagType.MIST]: MistTag;
[ArenaTagType.QUICK_GUARD]: QuickGuardTag;
[ArenaTagType.WIDE_GUARD]: WideGuardTag;
[ArenaTagType.MAT_BLOCK]: MatBlockTag;
[ArenaTagType.CRAFTY_SHIELD]: CraftyShieldTag;
[ArenaTagType.NO_CRIT]: NoCritTag;
[ArenaTagType.TOXIC_SPIKES]: ToxicSpikesTag;
[ArenaTagType.STEALTH_ROCK]: StealthRockTag;
[ArenaTagType.STICKY_WEB]: StickyWebTag;
[ArenaTagType.TRICK_ROOM]: TrickRoomTag;
[ArenaTagType.GRAVITY]: GravityTag;
[ArenaTagType.REFLECT]: ReflectTag;
[ArenaTagType.LIGHT_SCREEN]: LightScreenTag;
[ArenaTagType.AURORA_VEIL]: AuroraVeilTag;
[ArenaTagType.TAILWIND]: TailwindTag;
[ArenaTagType.HAPPY_HOUR]: HappyHourTag;
[ArenaTagType.SAFEGUARD]: SafeguardTag;
[ArenaTagType.IMPRISON]: ImprisonTag;
[ArenaTagType.FIRE_GRASS_PLEDGE]: FireGrassPledgeTag;
[ArenaTagType.WATER_FIRE_PLEDGE]: WaterFirePledgeTag;
[ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag;
[ArenaTagType.FAIRY_LOCK]: FairyLockTag;
[ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag;
[ArenaTagType.NONE]: NoneTag;
};

View File

@ -74,10 +74,13 @@ export class BattlerTag {
/** /**
* Tick down this {@linkcode BattlerTag}'s duration. * Tick down this {@linkcode BattlerTag}'s duration.
* @returns `true` if the tag should be kept (`turnCount > 0`) * @param _pokemon - The {@linkcode Pokemon} whom this tag belongs to.
* Unused by default but can be used by subclasses.
* @param _lapseType - The {@linkcode BattlerTagLapseType} being lapsed.
* Unused by default but can be used by subclasses.
* @returns `true` if the tag should be kept (`turnCount` > 0`)
*/ */
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
// TODO: Maybe flip this (return `true` if tag needs removal)
return --this.turnCount > 0; return --this.turnCount > 0;
} }
@ -123,7 +126,7 @@ export interface TerrainBattlerTag {
/** /**
* Base class for tags that restrict the usage of moves. This effect is generally referred to as "disabling" a move * Base class for tags that restrict the usage of moves. This effect is generally referred to as "disabling" a move
* in-game. This is not to be confused with {@linkcode MoveId.DISABLE}. * in-game (not to be confused with {@linkcode MoveId.DISABLE}).
* *
* Descendants can override {@linkcode isMoveRestricted} to restrict moves that * Descendants can override {@linkcode isMoveRestricted} to restrict moves that
* match a condition. A restricted move gets cancelled before it is used. * match a condition. A restricted move gets cancelled before it is used.

View File

@ -168,9 +168,9 @@ export abstract class Move implements Localizable {
} }
/** /**
* Get all move attributes that match `attrType` * Get all move attributes that match `attrType`.
* @param attrType any attribute that extends {@linkcode MoveAttr} * @param attrType - The name of a {@linkcode MoveAttr} to search for
* @returns Array of attributes that match `attrType`, Empty Array if none match. * @returns An array containing all attributes matching `attrType`, or an empty array if none match.
*/ */
getAttrs<T extends MoveAttrString>(attrType: T): (MoveAttrMap[T])[] { getAttrs<T extends MoveAttrString>(attrType: T): (MoveAttrMap[T])[] {
const targetAttr = MoveAttrs[attrType]; const targetAttr = MoveAttrs[attrType];
@ -181,9 +181,9 @@ export abstract class Move implements Localizable {
} }
/** /**
* Check if a move has an attribute that matches `attrType` * Check if a move has an attribute that matches `attrType`.
* @param attrType any attribute that extends {@linkcode MoveAttr} * @param attrType - The name of a {@linkcode MoveAttr} to search for
* @returns true if the move has attribute `attrType` * @returns Whether this move has at least 1 attribute that matches `attrType`
*/ */
hasAttr(attrType: MoveAttrString): boolean { hasAttr(attrType: MoveAttrString): boolean {
const targetAttr = MoveAttrs[attrType]; const targetAttr = MoveAttrs[attrType];
@ -195,23 +195,25 @@ export abstract class Move implements Localizable {
} }
/** /**
* Takes as input a boolean function and returns the first MoveAttr in attrs that matches true * Find the first attribute that matches a given predicate function.
* @param attrPredicate * @param attrPredicate - The predicate function to search `MoveAttr`s by
* @returns the first {@linkcode MoveAttr} element in attrs that makes the input function return true * @returns The first {@linkcode MoveAttr} for which `attrPredicate` returns `true`
*/ */
findAttr(attrPredicate: (attr: MoveAttr) => boolean): MoveAttr { findAttr(attrPredicate: (attr: MoveAttr) => boolean): MoveAttr {
return this.attrs.find(attrPredicate)!; // TODO: is the bang correct? // TODO: Remove bang and make return type `MoveAttr | undefined`,
// as well as add overload for functions of type `x is T`
return this.attrs.find(attrPredicate)!;
} }
/** /**
* Adds a new MoveAttr to the move (appends to the attr array) * Adds a new MoveAttr to this move (appends to the attr array).
* if the MoveAttr also comes with a condition, also adds that to the conditions array: {@linkcode MoveCondition} * If the MoveAttr also comes with a condition, it is added to its {@linkcode MoveCondition} array.
* @param AttrType {@linkcode MoveAttr} the constructor of a MoveAttr class * @param attrType - The {@linkcode MoveAttr} to add
* @param args the args needed to instantiate a the given class * @param args - The arguments needed to instantiate the given class
* @returns the called object {@linkcode Move} * @returns `this`
*/ */
attr<T extends Constructor<MoveAttr>>(AttrType: T, ...args: ConstructorParameters<T>): this { attr<T extends Constructor<MoveAttr>>(attrType: T, ...args: ConstructorParameters<T>): this {
const attr = new AttrType(...args); const attr = new attrType(...args);
this.attrs.push(attr); this.attrs.push(attr);
let attrCondition = attr.getCondition(); let attrCondition = attr.getCondition();
if (attrCondition) { if (attrCondition) {
@ -225,11 +227,13 @@ export abstract class Move implements Localizable {
} }
/** /**
* Adds a new MoveAttr to the move (appends to the attr array) * Adds a new MoveAttr to this move (appends to the attr array).
* if the MoveAttr also comes with a condition, also adds that to the conditions array: {@linkcode MoveCondition} * If the MoveAttr also comes with a condition, it is added to its {@linkcode MoveCondition} array.
* Almost identical to {@link attr}, except you are passing in a MoveAttr object, instead of a constructor and it's arguments *
* @param attrAdd {@linkcode MoveAttr} the attribute to add * Similar to {@linkcode attr}, except this takes an already instantiated {@linkcode MoveAttr} object
* @returns the called object {@linkcode Move} * as opposed to a constructor and its arguments.
* @param attrAdd - The {@linkcode MoveAttr} to add
* @returns `this`
*/ */
addAttr(attrAdd: MoveAttr): this { addAttr(attrAdd: MoveAttr): this {
this.attrs.push(attrAdd); this.attrs.push(attrAdd);
@ -246,8 +250,8 @@ export abstract class Move implements Localizable {
/** /**
* Sets the move target of this move * Sets the move target of this move
* @param moveTarget {@linkcode MoveTarget} the move target to set * @param moveTarget - The {@linkcode MoveTarget} to set
* @returns the called object {@linkcode Move} * @returns `this`
*/ */
target(moveTarget: MoveTarget): this { target(moveTarget: MoveTarget): this {
this.moveTarget = moveTarget; this.moveTarget = moveTarget;
@ -255,13 +259,13 @@ export abstract class Move implements Localizable {
} }
/** /**
* Getter function that returns if this Move has a MoveFlag * Getter function that returns if this Move has a given MoveFlag.
* @param flag {@linkcode MoveFlags} to check * @param flag - The {@linkcode MoveFlags} to check
* @returns boolean * @returns Whether this Move has the specified flag.
*/ */
hasFlag(flag: MoveFlags): boolean { hasFlag(flag: MoveFlags): boolean {
// internally it is taking the bitwise AND (MoveFlags are represented as bit-shifts) and returning False if result is 0 and true otherwise // Flags are internally represented as bitmasks, so we check by taking the bitwise AND.
return !!(this.flags & flag); return (this.flags & flag) !== MoveFlags.NONE;
} }
/** /**
@ -306,13 +310,13 @@ export abstract class Move implements Localizable {
} }
/** /**
* Checks if the move is immune to certain types. * Checks if the target is immune to this Move's type.
*
* Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster. * Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster.
* @param user - The source of this move * @param user - The {@linkcode Pokemon} using this move
* @param target - The target of this move * @param target - The {@linkcode Pokemon} targeted by this move
* @param type - The type of the move's target * @param type - The {@linkcode PokemonType} of the target
* @returns boolean * @returns Whether the move is blocked by the target's type.
* Self-targeted moves will return `false` regardless of circumstances.
*/ */
isTypeImmune(user: Pokemon, target: Pokemon, type: PokemonType): boolean { isTypeImmune(user: Pokemon, target: Pokemon, type: PokemonType): boolean {
if (this.moveTarget === MoveTarget.USER) { if (this.moveTarget === MoveTarget.USER) {
@ -326,7 +330,7 @@ export abstract class Move implements Localizable {
} }
break; break;
case PokemonType.DARK: case PokemonType.DARK:
if (user.hasAbility(AbilityId.PRANKSTER) && this.category === MoveCategory.STATUS && (user.isPlayer() !== target.isPlayer())) { if (user.hasAbility(AbilityId.PRANKSTER) && this.category === MoveCategory.STATUS && user.isOpponent(target)) {
return true; return true;
} }
break; break;
@ -336,9 +340,9 @@ export abstract class Move implements Localizable {
/** /**
* Checks if the move would hit its target's Substitute instead of the target itself. * Checks if the move would hit its target's Substitute instead of the target itself.
* @param user The {@linkcode Pokemon} using this move * @param user - The {@linkcode Pokemon} using this move
* @param target The {@linkcode Pokemon} targeted by this move * @param target - The {@linkcode Pokemon} targeted by this move
* @returns `true` if the move can bypass the target's Substitute; `false` otherwise. * @returns Whether this Move will hit the target's Substitute (assuming one exists).
*/ */
hitsSubstitute(user: Pokemon, target?: Pokemon): boolean { hitsSubstitute(user: Pokemon, target?: Pokemon): boolean {
if ([ MoveTarget.USER, MoveTarget.USER_SIDE, MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.moveTarget) if ([ MoveTarget.USER, MoveTarget.USER_SIDE, MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.moveTarget)
@ -356,13 +360,14 @@ export abstract class Move implements Localizable {
} }
/** /**
* Adds a move condition to the move * Adds a condition to this move (in addition to any provided by its prior {@linkcode MoveAttr}s).
* @param condition {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}, appends to conditions array a new MoveCondition object * The move will fail upon use if at least 1 of its conditions is not met.
* @returns the called object {@linkcode Move} * @param condition - The {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} to add to the conditions array.
* @returns `this`
*/ */
condition(condition: MoveCondition | MoveConditionFunc): this { condition(condition: MoveCondition | MoveConditionFunc): this {
if (typeof condition === "function") { if (typeof condition === "function") {
condition = new MoveCondition(condition as MoveConditionFunc); condition = new MoveCondition(condition);
} }
this.conditions.push(condition); this.conditions.push(condition);
@ -370,16 +375,22 @@ export abstract class Move implements Localizable {
} }
/** /**
* Internal dev flag for documenting edge cases. When using this, please document the known edge case. * Mark a move as having one or more edge cases.
* @returns the called object {@linkcode Move} * The move may lack certain niche interactions with other moves/abilities,
* but still functions as intended in most cases.
*
* When using this, **make sure to document the edge case** (or else this becomes pointless).
* @returns `this`
*/ */
edgeCase(): this { edgeCase(): this {
return this; return this;
} }
/** /**
* Marks the move as "partial": appends texts to the move name * Mark this move as partially implemented.
* @returns the called object {@linkcode Move} * Partial moves are expected to have some core functionality implemented, but may lack
* certain notable features or interactions with other moves or abilities.
* @returns `this`
*/ */
partial(): this { partial(): this {
this.nameAppend += " (P)"; this.nameAppend += " (P)";
@ -387,8 +398,10 @@ export abstract class Move implements Localizable {
} }
/** /**
* Marks the move as "unimplemented": appends texts to the move name * Mark this move as unimplemented.
* @returns the called object {@linkcode Move} * Unimplemented moves are ones which have _none_ of their basic functionality enabled,
* and cannot be used.
* @returns `this`
*/ */
unimplemented(): this { unimplemented(): this {
this.nameAppend += " (N)"; this.nameAppend += " (N)";
@ -963,10 +976,8 @@ export class AttackMove extends Move {
constructor(id: MoveId, type: PokemonType, category: MoveCategory, power: number, accuracy: number, pp: number, chance: number, priority: number, generation: number) { constructor(id: MoveId, type: PokemonType, category: MoveCategory, power: number, accuracy: number, pp: number, chance: number, priority: number, generation: number) {
super(id, type, category, MoveTarget.NEAR_OTHER, power, accuracy, pp, chance, priority, generation); super(id, type, category, MoveTarget.NEAR_OTHER, power, accuracy, pp, chance, priority, generation);
/** // > All damaging Fire-type moves can... thaw a frozen target, regardless of whether or not they have a chance to burn.
* {@link https://bulbapedia.bulbagarden.net/wiki/Freeze_(status_condition)} // - https://bulbapedia.bulbagarden.net/wiki/Freeze_(status_condition)
* > All damaging Fire-type moves can now thaw a frozen target, regardless of whether or not they have a chance to burn;
*/
if (this.type === PokemonType.FIRE) { if (this.type === PokemonType.FIRE) {
this.addAttr(new HealStatusEffectAttr(false, StatusEffect.FREEZE)); this.addAttr(new HealStatusEffectAttr(false, StatusEffect.FREEZE));
} }
@ -1222,7 +1233,8 @@ interface MoveEffectAttrOptions {
effectChanceOverride?: number; effectChanceOverride?: number;
} }
/** Base class defining all Move Effect Attributes /**
* Base class defining all Move Effect Attributes
* @extends MoveAttr * @extends MoveAttr
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
@ -1240,8 +1252,7 @@ export class MoveEffectAttr extends MoveAttr {
/** /**
* Defines when this effect should trigger in the move's effect order. * Defines when this effect should trigger in the move's effect order.
* @default MoveEffectTrigger.POST_APPLY * @defaultValue {@linkcode MoveEffectTrigger.POST_APPLY}
* @see {@linkcode MoveEffectTrigger}
*/ */
public get trigger () { public get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY; return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY;
@ -1250,7 +1261,7 @@ export class MoveEffectAttr extends MoveAttr {
/** /**
* `true` if this effect should only trigger on the first hit of * `true` if this effect should only trigger on the first hit of
* multi-hit moves. * multi-hit moves.
* @default false * @defaultValue `false`
*/ */
public get firstHitOnly () { public get firstHitOnly () {
return this.options?.firstHitOnly ?? false; return this.options?.firstHitOnly ?? false;
@ -1259,7 +1270,7 @@ export class MoveEffectAttr extends MoveAttr {
/** /**
* `true` if this effect should only trigger on the last hit of * `true` if this effect should only trigger on the last hit of
* multi-hit moves. * multi-hit moves.
* @default false * @defaultValue `false`
*/ */
public get lastHitOnly () { public get lastHitOnly () {
return this.options?.lastHitOnly ?? false; return this.options?.lastHitOnly ?? false;
@ -1268,7 +1279,7 @@ export class MoveEffectAttr extends MoveAttr {
/** /**
* `true` if this effect should apply only upon hitting a target * `true` if this effect should apply only upon hitting a target
* for the first time when targeting multiple {@linkcode Pokemon}. * for the first time when targeting multiple {@linkcode Pokemon}.
* @default false * @defaultValue `false`
*/ */
public get firstTargetOnly () { public get firstTargetOnly () {
return this.options?.firstTargetOnly ?? false; return this.options?.firstTargetOnly ?? false;
@ -2572,9 +2583,12 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
} }
/** /**
* Applies the effect of Psycho Shift to its target * Applies the effect of {@linkcode Moves.PSYCHO_SHIFT} to its target.
* Psycho Shift takes the user's status effect and passes it onto the target. The user is then healed after the move has been successfully executed. * Psycho Shift takes the user's status effect and passes it onto the target.
* @returns `true` if Psycho Shift's effect is able to be applied to the target * The user is then healed after the move has been successfully executed.
* @param user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} targeted by the move.
* @returns - Whether the effect was successfully applied to the target.
*/ */
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined); const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
@ -2907,6 +2921,12 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
} }
} }
/**
* Attribute to add the {@linkcode BattlerTagType.BYPASS_SLEEP | BYPASS_SLEEP Battler Tag} for 1 turn to the user before move use.
* Used by {@linkcode Moves.SNORE} and {@linkcode Moves.SLEEP_TALK}.
*/
// TODO: Should this use a battler tag?
// TODO: Give this `userSleptOrComatoseCondition` by default
export class BypassSleepAttr extends MoveAttr { export class BypassSleepAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (user.status?.effect === StatusEffect.SLEEP) { if (user.status?.effect === StatusEffect.SLEEP) {
@ -2924,7 +2944,7 @@ export class BypassSleepAttr extends MoveAttr {
* @param move * @param move
*/ */
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return user.status && user.status.effect === StatusEffect.SLEEP ? 200 : -10; return user.status?.effect === StatusEffect.SLEEP ? 200 : -10;
} }
} }
@ -3309,7 +3329,7 @@ export class StatStageChangeAttr extends MoveEffectAttr {
/** /**
* `true` to display a message for the stat change. * `true` to display a message for the stat change.
* @default true * @defaultValue `true`
*/ */
private get showMessage () { private get showMessage () {
return this.options?.showMessage ?? true; return this.options?.showMessage ?? true;
@ -5472,7 +5492,10 @@ export class NoEffectAttr extends MoveAttr {
} }
} }
const crashDamageFunc = (user: Pokemon, move: Move) => { /**
* Function to deal Crash Damage (1/2 max hp) to the user on apply.
*/
const crashDamageFunc: UserMoveConditionFunc = (user: Pokemon, move: Move) => {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled}); applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled});
if (cancelled.value) { if (cancelled.value) {
@ -7111,7 +7134,8 @@ export class CopyMoveAttr extends CallMoveAttr {
/** /**
* Attribute used for moves that cause the target to repeat their last used move. * Attribute used for moves that cause the target to repeat their last used move.
* *
* Used for [Instruct](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move)). * Used by {@linkcode Moves.INSTRUCT | Instruct}.
* @see [Instruct on Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move))
*/ */
export class RepeatMoveAttr extends MoveEffectAttr { export class RepeatMoveAttr extends MoveEffectAttr {
private movesetMove: PokemonMove; private movesetMove: PokemonMove;

View File

@ -36,13 +36,13 @@ export class PokemonMove {
} }
/** /**
* Checks whether the move can be selected or performed by a Pokemon, without consideration for the move's targets. * Checks whether this move can be selected/performed by a Pokemon, without consideration for the move's targets.
* The move is unusable if it is out of PP, restricted by an effect, or unimplemented. * The move is unusable if it is out of PP, restricted by an effect, or unimplemented.
* *
* @param pokemon - {@linkcode Pokemon} that would be using this move * @param pokemon - The {@linkcode Pokemon} attempting to use this move
* @param ignorePp - If `true`, skips the PP check * @param ignorePp - Whether to ignore checking if the move is out of PP; default `false`
* @param ignoreRestrictionTags - If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag}) * @param ignoreRestrictionTags - Whether to skip checks for {@linkcode MoveRestrictionBattlerTag}s; default `false`
* @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`. * @returns Whether this {@linkcode PokemonMove} can be selected by this Pokemon.
*/ */
isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean { isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean {
// TODO: Add Sky Drop's 1 turn stall // TODO: Add Sky Drop's 1 turn stall

View File

@ -406,12 +406,12 @@ export async function applyModifierTypeToPlayerPokemon(
// Check if the Pokemon has max stacks of that item already // Check if the Pokemon has max stacks of that item already
const modifier = modType.newModifier(pokemon); const modifier = modType.newModifier(pokemon);
const existing = globalScene.findModifier( const existing = globalScene.findModifier(
m => (m): m is PokemonHeldItemModifier =>
m instanceof PokemonHeldItemModifier && m instanceof PokemonHeldItemModifier &&
m.type.id === modType.id && m.type.id === modType.id &&
m.pokemonId === pokemon.id && m.pokemonId === pokemon.id &&
m.matchType(modifier), m.matchType(modifier),
) as PokemonHeldItemModifier; ) as PokemonHeldItemModifier | undefined;
// At max stacks // At max stacks
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { if (existing && existing.getStackCount() >= existing.getMaxStackCount()) {

View File

@ -13,6 +13,11 @@ export enum TerrainType {
PSYCHIC, PSYCHIC,
} }
export interface SerializedTerrain {
terrainType: TerrainType;
turnsLeft: number;
}
export class Terrain { export class Terrain {
public terrainType: TerrainType; public terrainType: TerrainType;
public turnsLeft: number; public turnsLeft: number;

View File

@ -11,6 +11,11 @@ import type { Move } from "#moves/move";
import { randSeedInt } from "#utils/common"; import { randSeedInt } from "#utils/common";
import i18next from "i18next"; import i18next from "i18next";
export interface SerializedWeather {
weatherType: WeatherType;
turnsLeft: number;
}
export class Weather { export class Weather {
public weatherType: WeatherType; public weatherType: WeatherType;
public turnsLeft: number; public turnsLeft: number;

View File

@ -1,3 +1,7 @@
/**
* A list of possible flags that various moves may have.
* Represented internally as a bitmask.
*/
export enum MoveFlags { export enum MoveFlags {
NONE = 0, NONE = 0,
MAKES_CONTACT = 1 << 0, MAKES_CONTACT = 1 << 0,

View File

@ -1,3 +1,13 @@
import type { ArenaTagTypeMap } from "#data/arena-tag";
import type { NonSerializableArenaTagType, SerializableArenaTagType } from "#types/arena-tags";
/**
* Enum representing all different types of {@linkcode ArenaTag}s.
* @privateRemarks
* When modifying the fields in this enum, ensure that:
* - The entry is added to / removed from {@linkcode ArenaTagTypeMap}
* - The tag is added to / removed from {@linkcode NonSerializableArenaTagType} or {@linkcode SerializableArenaTagType}
*/
export enum ArenaTagType { export enum ArenaTagType {
NONE = "NONE", NONE = "NONE",
MUD_SPORT = "MUD_SPORT", MUD_SPORT = "MUD_SPORT",

View File

@ -1,9 +1,7 @@
/** /** Enum for selected battle style. */
* Determines the selected battle style.
* - 'Switch' - The option to switch the active pokemon at the start of a battle will be displayed.
* - 'Set' - The option to switch the active pokemon at the start of a battle will not display.
*/
export enum BattleStyle { export enum BattleStyle {
SWITCH, /** Display option to switch active pokemon at battle start. */
SET SWITCH,
/** Hide option to switch active pokemon at battle start. */
SET
} }

View File

@ -1,3 +1,7 @@
/**
* The index of a given Pokemon on-field.
* Used as an index into `globalScene.getField`, as well as for most target-specifying effects.
*/
export enum BattlerIndex { export enum BattlerIndex {
ATTACKER = -1, ATTACKER = -1,
PLAYER, PLAYER,

View File

@ -1,15 +1,4 @@
/** /** Defines the speed of gaining experience. */
* Defines the speed of gaining experience.
*
* @remarks
* The `expGainSpeed` can have several modes:
* - `0` - Default: The normal speed.
* - `1` - Fast: Fast speed.
* - `2` - Faster: Faster speed.
* - `3` - Skip: Skip gaining exp animation.
*
* @default 0 - Uses the default normal speed.
*/
export enum ExpGainsSpeed { export enum ExpGainsSpeed {
/** The normal speed. */ /** The normal speed. */
DEFAULT, DEFAULT,

View File

@ -1,11 +1,9 @@
/** /** Enum for party experience gain notification style. */
* Determines exp notification style.
* - Default - the normal exp gain display, nothing changed
* - Only level up - we display the level up in the small frame instead of a message
* - Skip - no level up frame nor message
*/
export enum ExpNotification { export enum ExpNotification {
DEFAULT, /** Display amount flyout for all off-field party members upon gaining any amount of EXP. */
ONLY_LEVEL_UP, DEFAULT,
SKIP /** Display smaller flyout showing level gained on gaining a new level. */
ONLY_LEVEL_UP,
/** Do not show any flyouts for EXP gains or levelups. */
SKIP
} }

View File

@ -33,6 +33,7 @@ import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEven
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import { FieldEffectModifier } from "#modifiers/modifier"; import { FieldEffectModifier } from "#modifiers/modifier";
import type { Move } from "#moves/move"; import type { Move } from "#moves/move";
import type { AbstractConstructor } from "#types/type-helpers";
import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common"; import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
@ -652,7 +653,7 @@ export class Arena {
* @param args array of parameters that the called upon tags may need * @param args array of parameters that the called upon tags may need
*/ */
applyTagsForSide( applyTagsForSide(
tagType: ArenaTagType | Constructor<ArenaTag>, tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
side: ArenaTagSide, side: ArenaTagSide,
simulated: boolean, simulated: boolean,
...args: unknown[] ...args: unknown[]
@ -674,7 +675,11 @@ export class Arena {
* @param simulated if `true`, this applies arena tags without changing game state * @param simulated if `true`, this applies arena tags without changing game state
* @param args array of parameters that the called upon tags may need * @param args array of parameters that the called upon tags may need
*/ */
applyTags(tagType: ArenaTagType | Constructor<ArenaTag>, simulated: boolean, ...args: unknown[]): void { applyTags(
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
simulated: boolean,
...args: unknown[]
): void {
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args); this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args);
} }
@ -727,21 +732,19 @@ export class Arena {
/** /**
* Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides * Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides
* @param tagType The {@linkcode ArenaTagType} to retrieve * @param tagType - The {@linkcode ArenaTagType} to retrieve
* @returns The existing {@linkcode ArenaTag}, or `undefined` if not present. * @returns The existing {@linkcode ArenaTag}, or `undefined` if not present.
* @overload * @overload
*/ */
getTag(tagType: ArenaTagType): ArenaTag | undefined; getTag(tagType: ArenaTagType): ArenaTag | undefined;
/** /**
* Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides * Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides
* @param tagType The {@linkcode ArenaTag} to retrieve * @param tagType - The constructor of the {@linkcode ArenaTag} to retrieve
* @returns The existing {@linkcode ArenaTag}, or `undefined` if not present. * @returns The existing {@linkcode ArenaTag}, or `undefined` if not present.
* @overload * @overload
*/ */
getTag<T extends ArenaTag>(tagType: Constructor<T>): T | undefined; getTag<T extends ArenaTag>(tagType: Constructor<T> | AbstractConstructor<T>): T | undefined;
getTag(tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>): ArenaTag | undefined {
getTag(tagType: ArenaTagType | Constructor<ArenaTag>): ArenaTag | undefined {
return this.getTagOnSide(tagType, ArenaTagSide.BOTH); return this.getTagOnSide(tagType, ArenaTagSide.BOTH);
} }
@ -757,7 +760,10 @@ export class Arena {
* @param side The {@linkcode ArenaTagSide} to look at * @param side The {@linkcode ArenaTagSide} to look at
* @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there * @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there
*/ */
getTagOnSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide): ArenaTag | undefined { getTagOnSide(
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
side: ArenaTagSide,
): ArenaTag | undefined {
return typeof tagType === "string" return typeof tagType === "string"
? this.tags.find( ? this.tags.find(
t => t.tagType === tagType && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side), t => t.tagType === tagType && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side),

View File

@ -375,7 +375,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.ivs = ivs || getIvsFromId(this.id); this.ivs = ivs || getIvsFromId(this.id);
if (this.gender === undefined) { if (this.gender === undefined) {
this.generateGender(); this.gender = this.species.generateGender();
} }
if (this.formIndex === undefined) { if (this.formIndex === undefined) {
@ -437,7 +437,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* @param useIllusion - Whether we want the fake name or the real name of the Pokemon (for Illusion ability). * Return the name that will be displayed when this Pokemon is sent out into battle.
* @param useIllusion - Whether to consider this Pokemon's illusion if present; default `true`
* @returns The name to render for this {@linkcode Pokemon}.
*/ */
getNameToRender(useIllusion = true) { getNameToRender(useIllusion = true) {
const name: string = const name: string =
@ -446,7 +448,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
!useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.nickname : this.nickname; !useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.nickname : this.nickname;
try { try {
if (nickname) { if (nickname) {
return decodeURIComponent(escape(atob(nickname))); return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually...
} }
return name; return name;
} catch (err) { } catch (err) {
@ -455,11 +457,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
getPokeball(useIllusion = false) { /**
if (useIllusion) { * Return this Pokemon's {@linkcode PokeballType}.
return this.summonData.illusion?.pokeball ?? this.pokeball; * @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false`
} * @returns The {@linkcode PokeballType} that will be shown when this Pokemon is sent out into battle.
return this.pokeball; */
getPokeball(useIllusion = false): PokeballType {
return useIllusion && this.summonData.illusion ? this.summonData.illusion.pokeball : this.pokeball;
} }
init(): void { init(): void {
@ -516,17 +520,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Checks if a pokemon is fainted (ie: its `hp <= 0`). * Checks if a pokemon is fainted (ie: its `hp <= 0`).
* It's usually better to call {@linkcode isAllowedInBattle()} * Usually should not be called directly in favor of calling {@linkcode isAllowedInBattle()}.
* @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT} * @param checkStatus - Whether to also check that the pokemon's status is {@linkcode StatusEffect.FAINT}; default `false`
* @returns `true` if the pokemon is fainted * @returns Whether this Pokemon is fainted, as described above.
*/ */
public isFainted(checkStatus = false): boolean { public isFainted(checkStatus = false): boolean {
return this.hp <= 0 && (!checkStatus || this.status?.effect === StatusEffect.FAINT); return this.hp <= 0 && (!checkStatus || this.status?.effect === StatusEffect.FAINT);
} }
/** /**
* Check if this pokemon is both not fainted and allowed to be in battle based on currently active challenges. * Check if this pokemon is both not fainted and allowed to be used based on currently active challenges.
* @returns {boolean} `true` if pokemon is allowed in battle * @returns Whether this Pokemon is allowed to partake in battle.
*/ */
public isAllowedInBattle(): boolean { public isAllowedInBattle(): boolean {
return !this.isFainted() && this.isAllowedInChallenge(); return !this.isFainted() && this.isAllowedInChallenge();
@ -534,8 +538,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Check if this pokemon is allowed based on any active challenges. * Check if this pokemon is allowed based on any active challenges.
* It's usually better to call {@linkcode isAllowedInBattle()} * Usually should not be called directly in favor of consulting {@linkcode isAllowedInBattle()}.
* @returns {boolean} `true` if pokemon is allowed in battle * @returns Whether this Pokemon is allowed under the current challenge conditions.
*/ */
public isAllowedInChallenge(): boolean { public isAllowedInChallenge(): boolean {
const challengeAllowed = new BooleanHolder(true); const challengeAllowed = new BooleanHolder(true);
@ -545,8 +549,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Checks if this {@linkcode Pokemon} is allowed in battle (ie: not fainted, and allowed under any active challenges). * Checks if this {@linkcode Pokemon} is allowed in battle (ie: not fainted, and allowed under any active challenges).
* @param onField `true` to also check if the pokemon is currently on the field; default `false` * @param onField - Whether to also check if the pokemon is currently on the field; default `false`
* @returns `true` if the pokemon is "active", as described above. * @returns Whether this pokemon is considered "active", as described above.
* Returns `false` if there is no active {@linkcode BattleScene} or the pokemon is disallowed. * Returns `false` if there is no active {@linkcode BattleScene} or the pokemon is disallowed.
*/ */
public isActive(onField = false): boolean { public isActive(onField = false): boolean {
@ -703,7 +707,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
abstract getBattlerIndex(): BattlerIndex; abstract getBattlerIndex(): BattlerIndex;
/** /**
* @param useIllusion - Whether we want the illusion or not. * Load all assets needed for this Pokemon's use in battle
* @param ignoreOverride - Whether to ignore overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `true`
* @param useIllusion - Whether to consider this pokemon's active illusion; default `false`
* @returns A promise that resolves once all the corresponding assets have been loaded.
*/ */
async loadAssets(ignoreOverride = true, useIllusion = false): Promise<void> { async loadAssets(ignoreOverride = true, useIllusion = false): Promise<void> {
/** Promises that are loading assets and can be run concurrently. */ /** Promises that are loading assets and can be run concurrently. */
@ -832,11 +839,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Attempt to process variant sprite. * Attempt to process variant sprite color caches.
* * @param cacheKey - the cache key for the variant color sprite
* @param cacheKey the cache key for the variant color sprite * @param useExpSprite - Whether experimental sprites should be used if present
* @param useExpSprite should the experimental sprite be used * @param battleSpritePath - the filename of the sprite
* @param battleSpritePath the filename of the sprite
*/ */
async populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { async populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) {
const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`; const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`;
@ -883,8 +889,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
return this.fusionSpecies.forms[this.fusionFormIndex].formKey; return this.fusionSpecies.forms[this.fusionFormIndex].formKey;
} }
getSpriteAtlasPath(ignoreOverride?: boolean): string { // TODO: Add more documentation for all these attributes.
// They may be all similar, but what each one actually _does_ is quite unclear at first glance
getSpriteAtlasPath(ignoreOverride = false): string {
const spriteId = this.getSpriteId(ignoreOverride).replace(/_{2}/g, "/"); const spriteId = this.getSpriteId(ignoreOverride).replace(/_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
} }
@ -1027,10 +1037,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Get this {@linkcode Pokemon}'s {@linkcode PokemonSpeciesForm}. * Return this Pokemon's {@linkcode PokemonSpeciesForm | SpeciesForm}.
* @param ignoreOverride - Whether to ignore overridden species from {@linkcode MoveId.TRANSFORM}, default `false`. * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* This overrides `useIllusion` if `true`. * and overrides `useIllusion`.
* @param useIllusion - `true` to use the speciesForm of the illusion; default `false`. * @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false`.
* @returns This Pokemon's {@linkcode PokemonSpeciesForm}.
*/ */
getSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm { getSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm {
if (!ignoreOverride && this.summonData.speciesForm) { if (!ignoreOverride && this.summonData.speciesForm) {
@ -1082,9 +1093,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* @param {boolean} useIllusion - Whether we want the fusionSpeciesForm of the illusion or not. * Return the {@linkcode PokemonSpeciesForm | SpeciesForm} of this Pokemon's fusion counterpart.
* @param ignoreOverride - Whether to ignore species overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @param useIllusion - Whether to consider the species of this Pokemon's illusion; default `false`
* @returns The {@linkcode PokemonSpeciesForm} of this Pokemon's fusion counterpart.
*/ */
getFusionSpeciesForm(ignoreOverride?: boolean, useIllusion = false): PokemonSpeciesForm { getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm {
const fusionSpecies: PokemonSpecies = const fusionSpecies: PokemonSpecies =
useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!; useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!;
const fusionFormIndex = const fusionFormIndex =
@ -1406,17 +1420,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* Calculates and retrieves the final value of a stat considering any held * Calculates and retrieves the final value of a stat considering any held
* items, move effects, opponent abilities, and whether there was a critical * items, move effects, opponent abilities, and whether there was a critical
* hit. * hit.
* @param stat the desired {@linkcode EffectiveStat} * @param stat - The desired {@linkcode EffectiveStat | Stat} to check.
* @param opponent the target {@linkcode Pokemon} * @param opponent - The {@linkcode Pokemon} being targeted, if applicable.
* @param move the {@linkcode Move} being used * @param move - The {@linkcode Move} being used, if any. Used to check ability ignoring effects and similar.
* @param ignoreAbility determines whether this Pokemon's abilities should be ignored during the stat calculation * @param ignoreAbility - Whether to ignore ability effects of the user; default `false`.
* @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation. * @param ignoreOppAbility - Whether to ignore ability effects of the target; default `false`.
* @param ignoreAllyAbility during an attack, determines whether the ally Pokemon's abilities should be ignored during the stat calculation. * @param ignoreAllyAbility - Whether to ignore ability effects of the user's allies; default `false`.
* @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param isCritical - Whether a critical hit has occurred or not; default `false`.
* @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering * If `true`, will nullify offensive stat drops or defensive stat boosts.
* @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` * @param simulated - Whether to nullify any effects that produce changes to game state during calculations; default `true`
* @returns the final in-battle value of a stat * @param ignoreHeldItems - Whether to ignore the user's held items during stat calculation; default `false`.
* @returns The final in-battle value for the given stat.
*/ */
// TODO: Replace the optional parameters with an object to make calling this method less cumbersome
getEffectiveStat( getEffectiveStat(
stat: EffectiveStat, stat: EffectiveStat,
opponent?: Pokemon, opponent?: Pokemon,
@ -1436,6 +1452,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// The Ruin abilities here are never ignored, but they reveal themselves on summon anyway // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway
const fieldApplied = new BooleanHolder(false); const fieldApplied = new BooleanHolder(false);
for (const pokemon of globalScene.getField(true)) { for (const pokemon of globalScene.getField(true)) {
// TODO: remove `canStack` toggle from ability as breaking out renders it useless
applyAbAttrs("FieldMultiplyStatAbAttr", { applyAbAttrs("FieldMultiplyStatAbAttr", {
pokemon, pokemon,
stat, stat,
@ -1448,6 +1465,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
break; break;
} }
} }
if (!ignoreAbility) { if (!ignoreAbility) {
applyAbAttrs("StatMultiplierAbAttr", { applyAbAttrs("StatMultiplierAbAttr", {
pokemon: this, pokemon: this,
@ -1647,9 +1665,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* @param useIllusion - Whether we want the fake or real gender (illusion ability). * Return this Pokemon's {@linkcode Gender}.
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns the {@linkcode Gender} of this {@linkcode Pokemon}.
*/ */
getGender(ignoreOverride?: boolean, useIllusion = false): Gender { getGender(ignoreOverride = false, useIllusion = false): Gender {
if (useIllusion && this.summonData.illusion) { if (useIllusion && this.summonData.illusion) {
return this.summonData.illusion.gender; return this.summonData.illusion.gender;
} }
@ -1660,9 +1681,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* @param useIllusion - Whether we want the fake or real gender (illusion ability). * Return this Pokemon's fusion's {@linkcode Gender}.
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns The {@linkcode Gender} of this {@linkcode Pokemon}'s fusion.
*/ */
getFusionGender(ignoreOverride?: boolean, useIllusion = false): Gender { getFusionGender(ignoreOverride = false, useIllusion = false): Gender {
if (useIllusion && this.summonData.illusion?.fusionGender) { if (useIllusion && this.summonData.illusion?.fusionGender) {
return this.summonData.illusion.fusionGender; return this.summonData.illusion.fusionGender;
} }
@ -1673,15 +1697,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* @param useIllusion - Whether we want the fake or real shininess (illusion ability). * Check whether this Pokemon is shiny.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns Whether this Pokemon is shiny
*/ */
isShiny(useIllusion = false): boolean { isShiny(useIllusion = false): boolean {
if (!useIllusion && this.summonData.illusion) { if (!useIllusion && this.summonData.illusion) {
return !!( return (
this.summonData.illusion.basePokemon?.shiny || this.summonData.illusion.basePokemon?.shiny ||
(this.summonData.illusion.fusionSpecies && this.summonData.illusion.basePokemon?.fusionShiny) (this.summonData.illusion.fusionSpecies && this.summonData.illusion.basePokemon?.fusionShiny) ||
false
); );
} }
return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny); return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny);
} }
@ -1700,9 +1728,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* * Check whether this Pokemon is doubly shiny (both normal and fusion are shiny).
* @param useIllusion - Whether we want the fake or real shininess (illusion ability). * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns `true` if the {@linkcode Pokemon} is shiny and the fusion is shiny as well, `false` otherwise * @returns Whether this pokemon's base and fusion counterparts are both shiny.
*/ */
isDoubleShiny(useIllusion = false): boolean { isDoubleShiny(useIllusion = false): boolean {
if (!useIllusion && this.summonData.illusion?.basePokemon) { if (!useIllusion && this.summonData.illusion?.basePokemon) {
@ -1712,11 +1740,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.summonData.illusion.basePokemon.fusionShiny this.summonData.illusion.basePokemon.fusionShiny
); );
} }
return this.isFusion(useIllusion) && this.shiny && this.fusionShiny; return this.isFusion(useIllusion) && this.shiny && this.fusionShiny;
} }
/** /**
* @param useIllusion - Whether we want the fake or real variant (illusion ability). * Return this Pokemon's {@linkcode Variant | shiny variant}.
* Only meaningful if this pokemon is actually shiny.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns The shiny variant of this Pokemon.
*/ */
getVariant(useIllusion = false): Variant { getVariant(useIllusion = false): Variant {
if (!useIllusion && this.summonData.illusion) { if (!useIllusion && this.summonData.illusion) {
@ -1724,9 +1756,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
? this.summonData.illusion.basePokemon!.variant ? this.summonData.illusion.basePokemon!.variant
: (Math.max(this.variant, this.fusionVariant) as Variant); : (Math.max(this.variant, this.fusionVariant) as Variant);
} }
return !this.isFusion(true) ? this.variant : (Math.max(this.variant, this.fusionVariant) as Variant); return !this.isFusion(true) ? this.variant : (Math.max(this.variant, this.fusionVariant) as Variant);
} }
// TODO: Clarify how this differs from `getVariant`
getBaseVariant(doubleShiny: boolean): Variant { getBaseVariant(doubleShiny: boolean): Variant {
if (doubleShiny) { if (doubleShiny) {
return this.summonData.illusion?.basePokemon?.variant ?? this.variant; return this.summonData.illusion?.basePokemon?.variant ?? this.variant;
@ -1734,19 +1768,28 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
return this.getVariant(); return this.getVariant();
} }
/**
* Return this pokemon's overall luck value, based on its shininess (1 pt per variant lvl).
* @returns The luck value of this Pokemon.
*/
getLuck(): number { getLuck(): number {
return this.luck + (this.isFusion() ? this.fusionLuck : 0); return this.luck + (this.isFusion() ? this.fusionLuck : 0);
} }
/**
* Return whether this {@linkcode Pokemon} is currently fused with anything.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns Whether this Pokemon is currently fused with another species.
*/
isFusion(useIllusion = false): boolean { isFusion(useIllusion = false): boolean {
if (useIllusion && this.summonData.illusion) { return useIllusion && this.summonData.illusion ? !!this.summonData.illusion.fusionSpecies : !!this.fusionSpecies;
return !!this.summonData.illusion.fusionSpecies;
}
return !!this.fusionSpecies;
} }
/** /**
* @param useIllusion - Whether we want the fake name or the real name of the Pokemon (for Illusion ability). * Return this {@linkcode Pokemon}'s name.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns This Pokemon's name.
* @see {@linkcode getNameToRender} - gets this Pokemon's display name.
*/ */
getName(useIllusion = false): string { getName(useIllusion = false): string {
return !useIllusion && this.summonData.illusion?.basePokemon return !useIllusion && this.summonData.illusion?.basePokemon
@ -1755,19 +1798,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Checks if the {@linkcode Pokemon} has a fusion with the specified {@linkcode SpeciesId}. * Check whether this {@linkcode Pokemon} has a fusion with the specified {@linkcode SpeciesId}.
* @param species the pokemon {@linkcode SpeciesId} to check * @param species - The {@linkcode SpeciesId} to check against.
* @returns `true` if the {@linkcode Pokemon} has a fusion with the specified {@linkcode SpeciesId}, `false` otherwise * @returns Whether this Pokemon is currently fused with the specified {@linkcode SpeciesId}.
*/ */
hasFusionSpecies(species: SpeciesId): boolean { hasFusionSpecies(species: SpeciesId): boolean {
return this.fusionSpecies?.speciesId === species; return this.fusionSpecies?.speciesId === species;
} }
/** /**
* Checks if the {@linkcode Pokemon} has is the specified {@linkcode SpeciesId} or is fused with it. * Check whether this {@linkcode Pokemon} either is or is fused with the given {@linkcode SpeciesId}.
* @param species the pokemon {@linkcode SpeciesId} to check * @param species - The {@linkcode SpeciesId} to check against.
* @param formKey If provided, requires the species to be in that form * @param formKey - If provided, will require the species to be in the given form.
* @returns `true` if the pokemon is the species or is fused with it, `false` otherwise * @returns Whether this Pokemon has this species as either its base or fusion counterpart.
*/ */
hasSpecies(species: SpeciesId, formKey?: string): boolean { hasSpecies(species: SpeciesId, formKey?: string): boolean {
if (isNullOrUndefined(formKey)) { if (isNullOrUndefined(formKey)) {
@ -1782,7 +1825,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
abstract isBoss(): boolean; abstract isBoss(): boolean;
getMoveset(ignoreOverride?: boolean): PokemonMove[] { /**
* Return all the {@linkcode PokemonMove}s that make up this Pokemon's moveset.
* Takes into account player/enemy moveset overrides (which will also override PP count).
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns An array of {@linkcode PokemonMove}, as described above.
*/
getMoveset(ignoreOverride = false): PokemonMove[] {
const ret = !ignoreOverride && this.summonData.moveset ? this.summonData.moveset : this.moveset; const ret = !ignoreOverride && this.summonData.moveset ? this.summonData.moveset : this.moveset;
// Overrides moveset based on arrays specified in overrides.ts // Overrides moveset based on arrays specified in overrides.ts
@ -1804,11 +1853,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Checks which egg moves have been unlocked for the {@linkcode Pokemon} based * Check which egg moves have been unlocked for this {@linkcode Pokemon}.
* on the species it was met at or by the first {@linkcode Pokemon} in its evolution * Looks at either the species it was met at or the first {@linkcode Species} in its evolution
* line that can act as a starter and provides those egg moves. * line that can act as a starter and provides those egg moves.
* @returns an array of {@linkcode MoveId}, the length of which is determined by how many * @returns An array of all {@linkcode MoveId}s that are egg moves and unlocked for this Pokemon.
* egg moves are unlocked for that species.
*/ */
getUnlockedEggMoves(): MoveId[] { getUnlockedEggMoves(): MoveId[] {
const moves: MoveId[] = []; const moves: MoveId[] = [];
@ -1825,13 +1873,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Gets all possible learnable level moves for the {@linkcode Pokemon}, * Get all possible learnable level moves for the {@linkcode Pokemon},
* excluding any moves already known. * excluding any moves already known.
* *
* Available egg moves are only included if the {@linkcode Pokemon} was * Available egg moves are only included if the {@linkcode Pokemon} was
* in the starting party of the run and if Fresh Start is not active. * in the starting party of the run and if Fresh Start is not active.
* @returns an array of {@linkcode MoveId}, the length of which is determined * @returns An array of {@linkcode MoveId}s, as described above.
* by how many learnable moves there are for the {@linkcode Pokemon}.
*/ */
public getLearnableLevelMoves(): MoveId[] { public getLearnableLevelMoves(): MoveId[] {
let levelMoves = this.getLevelMoves(1, true, false, true).map(lm => lm[1]); let levelMoves = this.getLevelMoves(1, true, false, true).map(lm => lm[1]);
@ -1846,12 +1893,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Gets the types of a pokemon * Evaluate and return this Pokemon's typing.
* @param includeTeraType - `true` to include tera-formed type; Default: `false` * @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `false`
* @param forDefend - `true` if the pokemon is defending from an attack; Default: `false` * @param forDefend - Whether this Pokemon is currently receiving an attack; default `false`
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @param useIllusion - `true` to return the types of the illusion instead of the actual types; Default: `false` * @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false`
* @returns array of {@linkcode PokemonType} * @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or percieved).
*/ */
public getTypes( public getTypes(
includeTeraType = false, includeTeraType = false,
@ -1947,7 +1994,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
// remove UNKNOWN if other types are present // remove UNKNOWN if other types are present
if (types.length > 1 && types.includes(PokemonType.UNKNOWN)) { if (types.length > 1) {
const index = types.indexOf(PokemonType.UNKNOWN); const index = types.indexOf(PokemonType.UNKNOWN);
if (index !== -1) { if (index !== -1) {
types.splice(index, 1); types.splice(index, 1);
@ -1968,24 +2015,25 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Checks if the pokemon's typing includes the specified type * Check if this Pokemon's typing includes the specified type.
* @param type - {@linkcode PokemonType} to check * @param type - The {@linkcode PokemonType} to check
* @param includeTeraType - `true` to include tera-formed type; Default: `true` * @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `true`
* @param forDefend - `true` if the pokemon is defending from an attack; Default: `false` * @param forDefend - Whether this Pokemon is currently receiving an attack; default `false`
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns `true` if the Pokemon's type matches * @returns Whether this Pokemon is of the specified type.
*/ */
public isOfType(type: PokemonType, includeTeraType = true, forDefend = false, ignoreOverride = false): boolean { public isOfType(type: PokemonType, includeTeraType = true, forDefend = false, ignoreOverride = false): boolean {
return this.getTypes(includeTeraType, forDefend, ignoreOverride).some(t => t === type); return this.getTypes(includeTeraType, forDefend, ignoreOverride).includes(type);
} }
/** /**
* Gets the non-passive ability of the pokemon. This accounts for fusions and ability changing effects. * Get this Pokemon's non-passive {@linkcode Ability}, factoring in fusions, overrides and ability-changing effects.
* This should rarely be called, most of the time {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr} are better used as
* those check both the passive and non-passive abilities and account for ability suppression. * Should rarely be called directly in favor of {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr},
* @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases * both of which check both ability slots and account for suppression.
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` * @see {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} are the intended ways to check abilities in most cases
* @returns The non-passive {@linkcode Ability} of the pokemon * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns The non-passive {@linkcode Ability} of this Pokemon.
*/ */
public getAbility(ignoreOverride = false): Ability { public getAbility(ignoreOverride = false): Ability {
if (!ignoreOverride && this.summonData.ability) { if (!ignoreOverride && this.summonData.ability) {
@ -2131,7 +2179,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (passive && !this.hasPassive()) { if (passive && !this.hasPassive()) {
return false; return false;
} }
const ability = !passive ? this.getAbility() : this.getPassiveAbility(); const ability = passive ? this.getPassiveAbility() : this.getAbility();
if (this.isFusion() && ability.hasAttr("NoFusionAbilityAbAttr")) { if (this.isFusion() && ability.hasAttr("NoFusionAbilityAbAttr")) {
return false; return false;
} }
@ -2144,7 +2192,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
const suppressAbilitiesTag = arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag; const suppressAbilitiesTag = arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag;
const suppressOffField = ability.hasAttr("PreSummonAbAttr"); const suppressOffField = ability.hasAttr("PreSummonAbAttr");
if ((this.isOnField() || suppressOffField) && suppressAbilitiesTag && !suppressAbilitiesTag.isBeingRemoved()) { if ((this.isOnField() || suppressOffField) && suppressAbilitiesTag && !suppressAbilitiesTag.beingRemoved) {
const thisAbilitySuppressing = ability.hasAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr"); const thisAbilitySuppressing = ability.hasAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr");
const hasSuppressingAbility = this.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false); const hasSuppressingAbility = this.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false);
// Neutralizing gas is up - suppress abilities unless they are unsuppressable or this pokemon is responsible for the gas // Neutralizing gas is up - suppress abilities unless they are unsuppressable or this pokemon is responsible for the gas
@ -2162,13 +2210,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Checks whether a pokemon has the specified ability and it's in effect. Accounts for all the various * Check whether a pokemon has the specified ability in effect, either as a normal or passive ability.
* effects which can affect whether an ability will be present or in effect, and both passive and * Accounts for all the various effects which can disable or modify abilities.
* non-passive. This is the primary way to check whether a pokemon has a particular ability. * @param ability - The {@linkcode Abilities | Ability} to check for
* @param ability The ability to check for
* @param canApply - Whether to check if the ability is currently active; default `true` * @param canApply - Whether to check if the ability is currently active; default `true`
* @param ignoreOverride Whether to ignore ability changing effects; default `false` * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns `true` if the ability is present and active * @returns Whether this {@linkcode Pokemon} has the given ability
*/ */
public hasAbility(ability: AbilityId, canApply = true, ignoreOverride = false): boolean { public hasAbility(ability: AbilityId, canApply = true, ignoreOverride = false): boolean {
if (this.getAbility(ignoreOverride).id === ability && (!canApply || this.canApplyAbility())) { if (this.getAbility(ignoreOverride).id === ability && (!canApply || this.canApplyAbility())) {
@ -2178,14 +2225,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Checks whether a pokemon has an ability with the specified attribute and it's in effect. * Check whether this pokemon has an ability with the specified attribute in effect, either as a normal or passive ability.
* Accounts for all the various effects which can affect whether an ability will be present or * Accounts for all the various effects which can disable or modify abilities.
* in effect, and both passive and non-passive. This is one of the two primary ways to check * @param attrType - The {@linkcode AbAttr | attribute} to check for
* whether a pokemon has a particular ability.
* @param attrType The {@link AbAttr | ability attribute} to check for
* @param canApply - Whether to check if the ability is currently active; default `true` * @param canApply - Whether to check if the ability is currently active; default `true`
* @param ignoreOverride Whether to ignore ability changing effects; default `false` * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns `true` if an ability with the given {@linkcode AbAttr} is present and active * @returns Whether this Pokemon has an ability with the given {@linkcode AbAttr}.
*/ */
public hasAbilityWithAttr(attrType: AbAttrString, canApply = true, ignoreOverride = false): boolean { public hasAbilityWithAttr(attrType: AbAttrString, canApply = true, ignoreOverride = false): boolean {
if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) { if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) {
@ -2207,7 +2252,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
const autotomizedTag = this.getTag(AutotomizedTag); const autotomizedTag = this.getTag(AutotomizedTag);
let weightRemoved = 0; let weightRemoved = 0;
if (!isNullOrUndefined(autotomizedTag)) { if (!isNullOrUndefined(autotomizedTag)) {
weightRemoved = 100 * autotomizedTag!.autotomizeCount; weightRemoved = 100 * autotomizedTag.autotomizeCount;
} }
const minWeight = 0.1; const minWeight = 0.1;
const weight = new NumberHolder(this.species.weight - weightRemoved); const weight = new NumberHolder(this.species.weight - weightRemoved);
@ -3403,10 +3448,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* *
* Note that this does not apply to evasion or accuracy * Note that this does not apply to evasion or accuracy
* @see {@linkcode getAccuracyMultiplier} * @see {@linkcode getAccuracyMultiplier}
* @param stat the desired {@linkcode EffectiveStat} * @param stat - The {@linkcode EffectiveStat} to calculate
* @param opponent the target {@linkcode Pokemon} * @param opponent - The {@linkcode Pokemon} being targeted
* @param move the {@linkcode Move} being used * @param move - The {@linkcode Move} being used
* @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default)
* @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param isCritical determines whether a critical hit has occurred or not (`false` by default)
* @param simulated determines whether effects are applied without altering game state (`true` by default) * @param simulated determines whether effects are applied without altering game state (`true` by default)
* @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false`
@ -4395,6 +4440,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
return null; return null;
} }
/**
* Return this Pokemon's move history.
* Entries are sorted in order of OLDEST to NEWEST
* @returns An array of {@linkcode TurnMove}, as described above.
* @see {@linkcode getLastXMoves}
*/
public getMoveHistory(): TurnMove[] { public getMoveHistory(): TurnMove[] {
return this.summonData.moveHistory; return this.summonData.moveHistory;
} }
@ -4408,19 +4459,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Returns a list of the most recent move entries in this Pokemon's move history. * Return a list of the most recent move entries in this {@linkcode Pokemon}'s move history.
* The retrieved move entries are sorted in order from NEWEST to OLDEST. * The retrieved move entries are sorted in order from **NEWEST** to **OLDEST**.
* @param moveCount The number of move entries to retrieve. * @param moveCount - The maximum number of move entries to retrieve.
* If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}). * If negative, retrieves the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}).
* Default is `1`. * Default is `1`.
* @returns A list of {@linkcode TurnMove}, as specified above. * @returns An array of {@linkcode TurnMove}, as specified above.
*/ */
// TODO: Update documentation in dancer PR to mention "getLastNonVirtualMove"
getLastXMoves(moveCount = 1): TurnMove[] { getLastXMoves(moveCount = 1): TurnMove[] {
const moveHistory = this.getMoveHistory(); const moveHistory = this.getMoveHistory();
if (moveCount >= 0) { if (moveCount > 0) {
return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse(); return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse();
} }
return moveHistory.slice(0).reverse(); return moveHistory.slice().reverse();
} }
/** /**
@ -5576,13 +5628,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Reduces one of this Pokemon's held item stacks by 1, and removes the item if applicable. * Reduces one of this Pokemon's held item stacks by 1, removing it if applicable.
* Does nothing if this Pokemon is somehow not the owner of the held item. * Does nothing if this Pokemon is somehow not the owner of the held item.
* @param heldItem The item stack to be reduced by 1. * @param heldItem - The item stack to be reduced.
* @param forBattle If `false`, do not trigger in-battle effects (such as Unburden) from losing the item. For example, set this to `false` if the Pokemon is giving away the held item for a Mystery Encounter. Default is `true`. * @param forBattle - Whether to trigger in-battle effects (such as Unburden) after losing the item. Default: `true`
* @returns `true` if the item was removed successfully, `false` otherwise. * Should be `false` for all item loss occurring outside of battle (MEs, etc.).
* @returns Whether the item was removed successfully.
*/ */
public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle = true): boolean { public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle = true): boolean {
// TODO: What does a -1 pokemon id mean?
if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) { if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) {
return false; return false;
} }
@ -6254,22 +6308,23 @@ export class EnemyPokemon extends Pokemon {
} }
/** /**
* Sets the pokemons boss status. If true initializes the boss segments either from the arguments * Set this {@linkcode EnemyPokemon}'s boss status.
* or through the the Scene.getEncounterBossSegments function
* *
* @param boss if the pokemon is a boss * @param boss - Whether this pokemon should be a boss; default `true`
* @param bossSegments amount of boss segments (health-bar segments) * @param bossSegments - Optional amount amount of health bar segments to give;
* will be generated by {@linkcode BattleScene.getEncounterBossSegments} if omitted
*/ */
setBoss(boss = true, bossSegments = 0): void { setBoss(boss = true, bossSegments?: number): void {
if (boss) { if (!boss) {
this.bossSegments =
bossSegments ||
globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, this.level, this.species, true);
this.bossSegmentIndex = this.bossSegments - 1;
} else {
this.bossSegments = 0; this.bossSegments = 0;
this.bossSegmentIndex = 0; this.bossSegmentIndex = 0;
return;
} }
this.bossSegments =
bossSegments ??
globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, this.level, this.species, true);
this.bossSegmentIndex = this.bossSegments - 1;
} }
generateAndPopulateMoveset(formIndex?: number): void { generateAndPopulateMoveset(formIndex?: number): void {

View File

@ -45,8 +45,10 @@ Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative
document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems")); document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems"));
// biome-ignore lint/suspicious/noImplicitAnyLet: TODO // biome-ignore lint/suspicious/noImplicitAnyLet: TODO
let game; let game;
// biome-ignore lint/suspicious/noImplicitAnyLet: TODO
let manifest;
const startGame = async (manifest?: any) => { const startGame = async () => {
await initI18n(); await initI18n();
const LoadingScene = (await import("./loading-scene")).LoadingScene; const LoadingScene = (await import("./loading-scene")).LoadingScene;
const BattleScene = (await import("./battle-scene")).BattleScene; const BattleScene = (await import("./battle-scene")).BattleScene;
@ -110,10 +112,13 @@ const startGame = async (manifest?: any) => {
fetch("/manifest.json") fetch("/manifest.json")
.then(res => res.json()) .then(res => res.json())
.then(jsonResponse => { .then(jsonResponse => {
startGame(jsonResponse.manifest); manifest = jsonResponse.manifest;
}) })
.catch(() => { .catch(err => {
// Manifest not found (likely local build) // Manifest not found (likely local build or path error on live)
console.log(`Manifest not found. ${err}`);
})
.finally(() => {
startGame(); startGame();
}); });

View File

@ -52,25 +52,11 @@ const iconOverflowIndex = 24;
export const modifierSortFunc = (a: Modifier, b: Modifier): number => { export const modifierSortFunc = (a: Modifier, b: Modifier): number => {
const itemNameMatch = a.type.name.localeCompare(b.type.name); const itemNameMatch = a.type.name.localeCompare(b.type.name);
const typeNameMatch = a.constructor.name.localeCompare(b.constructor.name); const typeNameMatch = a.constructor.name.localeCompare(b.constructor.name);
const aId = a instanceof PokemonHeldItemModifier && a.pokemonId ? a.pokemonId : 4294967295; const aId = a instanceof PokemonHeldItemModifier ? a.pokemonId : -1;
const bId = b instanceof PokemonHeldItemModifier && b.pokemonId ? b.pokemonId : 4294967295; const bId = b instanceof PokemonHeldItemModifier ? b.pokemonId : -1;
//First sort by pokemonID // First sort by pokemon ID, then by item type and then name
if (aId < bId) { return aId - bId || typeNameMatch || itemNameMatch;
return 1;
}
if (aId > bId) {
return -1;
}
if (aId === bId) {
//Then sort by item type
if (typeNameMatch === 0) {
return itemNameMatch;
//Finally sort by item name
}
return typeNameMatch;
}
return 0;
}; };
export class ModifierBar extends Phaser.GameObjects.Container { export class ModifierBar extends Phaser.GameObjects.Container {
@ -757,7 +743,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return 1; return 1;
} }
getMaxStackCount(forThreshold?: boolean): number { getMaxStackCount(forThreshold = false): number {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (!pokemon) { if (!pokemon) {
return 0; return 0;
@ -2814,6 +2800,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
damageMultiplier.value *= 1 - 0.25 * this.getStackCount(); damageMultiplier.value *= 1 - 0.25 * this.getStackCount();
return true; return true;
} }
if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== this.getStackCount() + 1) { if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== this.getStackCount() + 1) {
// Deal 25% damage for each remaining Multi Lens hit // Deal 25% damage for each remaining Multi Lens hit
damageMultiplier.value *= 0.25; damageMultiplier.value *= 0.25;

View File

@ -562,14 +562,13 @@ export class PhaseManager {
} }
/** /**
* Queues an ability bar flyout phase * Queue a phase to show or hide the ability flyout bar.
* @param pokemon The pokemon who has the ability * @param pokemon - The {@linkcode Pokemon} whose ability is being activated
* @param passive Whether the ability is a passive * @param passive - Whether the ability is a passive
* @param show Whether to show or hide the bar * @param show - Whether to show or hide the bar
*/ */
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void { public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase()); this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase());
this.clearPhaseQueueSplice();
} }
/** /**

View File

@ -88,7 +88,7 @@ export class CommandPhase extends FieldPhase {
} }
// Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP. // Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP.
const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag; const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag | undefined;
if (encoreTag) { if (encoreTag) {
this.getPokemon().lapseTag(BattlerTagType.ENCORE); this.getPokemon().lapseTag(BattlerTagType.ENCORE);
} }

View File

@ -37,7 +37,7 @@ export class SelectStarterPhase extends Phase {
/** /**
* Initialize starters before starting the first battle * Initialize starters before starting the first battle
* @param starters {@linkcode Pokemon} with which to start the first battle * @param starters - Array of {@linkcode Starter}s with which to start the battle
*/ */
initBattle(starters: Starter[]) { initBattle(starters: Starter[]) {
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();

View File

@ -204,7 +204,7 @@ export class TitlePhase extends Phase {
globalScene.eventManager.startEventChallenges(); globalScene.eventManager.startEventChallenges();
globalScene.setSeed(seed); globalScene.setSeed(seed);
globalScene.resetSeed(0); globalScene.resetSeed();
globalScene.money = globalScene.gameMode.getStartingMoney(); globalScene.money = globalScene.gameMode.getStartingMoney();
@ -283,6 +283,7 @@ export class TitlePhase extends Phase {
console.error("Failed to load daily run:\n", err); console.error("Failed to load daily run:\n", err);
}); });
} else { } else {
// Grab first 10 chars of ISO date format (YYYY-MM-DD) and convert to base64
let seed: string = btoa(new Date().toISOString().substring(0, 10)); let seed: string = btoa(new Date().toISOString().substring(0, 10));
if (!isNullOrUndefined(Overrides.DAILY_RUN_SEED_OVERRIDE)) { if (!isNullOrUndefined(Overrides.DAILY_RUN_SEED_OVERRIDE)) {
seed = Overrides.DAILY_RUN_SEED_OVERRIDE; seed = Overrides.DAILY_RUN_SEED_OVERRIDE;

View File

@ -1,10 +1,20 @@
import type { ArenaTag } from "#data/arena-tag"; import type { ArenaTag } from "#data/arena-tag";
import { loadArenaTag } from "#data/arena-tag"; import { loadArenaTag, SerializableArenaTag } from "#data/arena-tag";
import type { SerializedPositionalTag } from "#data/positional-tags/load-positional-tag"; import type { SerializedPositionalTag } from "#data/positional-tags/load-positional-tag";
import { Terrain } from "#data/terrain"; import { Terrain } from "#data/terrain";
import { Weather } from "#data/weather"; import { Weather } from "#data/weather";
import type { BiomeId } from "#enums/biome-id"; import type { BiomeId } from "#enums/biome-id";
import { Arena } from "#field/arena"; import { Arena } from "#field/arena";
import type { ArenaTagTypeData } from "#types/arena-tags";
import type { NonFunctionProperties } from "#types/type-helpers";
export interface SerializedArenaData {
biome: BiomeId;
weather: NonFunctionProperties<Weather> | null;
terrain: NonFunctionProperties<Terrain> | null;
tags?: ArenaTagTypeData[];
playerTerasUsed?: number;
}
export class ArenaData { export class ArenaData {
public biome: BiomeId; public biome: BiomeId;
@ -14,26 +24,27 @@ export class ArenaData {
public positionalTags: SerializedPositionalTag[] = []; public positionalTags: SerializedPositionalTag[] = [];
public playerTerasUsed: number; public playerTerasUsed: number;
constructor(source: Arena | any) { constructor(source: Arena | SerializedArenaData) {
const sourceArena = source instanceof Arena ? (source as Arena) : null; // Exclude any unserializable tags from the serialized data (such as ones only lasting 1 turn).
this.biome = sourceArena ? sourceArena.biomeType : source.biome; // NOTE: The filter has to be done _after_ map, data loaded from `ArenaTagTypeData`
this.weather = sourceArena // is not yet an instance of `ArenaTag`
? sourceArena.weather this.tags =
: source.weather source.tags
? new Weather(source.weather.weatherType, source.weather.turnsLeft) ?.map((t: ArenaTag | ArenaTagTypeData) => loadArenaTag(t))
: null; ?.filter((tag): tag is SerializableArenaTag => tag instanceof SerializableArenaTag) ?? [];
this.terrain = sourceArena
? sourceArena.terrain
: source.terrain
? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft)
: null;
this.playerTerasUsed = (sourceArena ? sourceArena.playerTerasUsed : source.playerTerasUsed) ?? 0;
this.tags = [];
if (source.tags) { this.playerTerasUsed = source.playerTerasUsed ?? 0;
this.tags = source.tags.map(t => loadArenaTag(t)); this.positionalTags = (sourceArena ? sourceArena.positionalTagManager.tags : source.positionalTags) ?? [];
if (source instanceof Arena) {
this.biome = source.biomeType;
this.weather = source.weather;
this.terrain = source.terrain;
return;
} }
this.positionalTags = (sourceArena ? sourceArena.positionalTagManager.tags : source.positionalTags) ?? []; this.biome = source.biome;
this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null;
this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null;
} }
} }

View File

@ -43,7 +43,7 @@ import * as Modifier from "#modifiers/modifier";
import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
import type { Variant } from "#sprites/variant"; import type { Variant } from "#sprites/variant";
import { achvs } from "#system/achv"; import { achvs } from "#system/achv";
import { ArenaData } from "#system/arena-data"; import { ArenaData, type SerializedArenaData } from "#system/arena-data";
import { ChallengeData } from "#system/challenge-data"; import { ChallengeData } from "#system/challenge-data";
import { EggData } from "#system/egg-data"; import { EggData } from "#system/egg-data";
import { GameStats } from "#system/game-stats"; import { GameStats } from "#system/game-stats";
@ -1252,7 +1252,8 @@ export class GameData {
// (or prevent them from being null) // (or prevent them from being null)
// If the value is able to *not exist*, it should say so in the code // If the value is able to *not exist*, it should say so in the code
const sessionData = JSON.parse(dataStr, (k: string, v: any) => { const sessionData = JSON.parse(dataStr, (k: string, v: any) => {
// TODO: Add pre-parse migrate scripts // TODO: Move this to occur _after_ migrate scripts (and refactor all non-assignment duties into migrate scripts)
// This should ideally be just a giant assign block
switch (k) { switch (k) {
case "party": case "party":
case "enemyParty": { case "enemyParty": {
@ -1290,7 +1291,7 @@ export class GameData {
} }
case "arena": case "arena":
return new ArenaData(v); return new ArenaData(v as SerializedArenaData);
case "challenges": { case "challenges": {
const ret: ChallengeData[] = []; const ret: ChallengeData[] = [];