From 56e3402c81266c9340274cb3703df35e7533133f Mon Sep 17 00:00:00 2001 From: podar <1999688+podarsmarty@users.noreply.github.com> Date: Tue, 22 Jul 2025 00:08:17 -0500 Subject: [PATCH 1/3] [Bug] [QoL] Updating manifest fetching to be more resilient for local development and offline builds. (#4735) * Fixing local development and offline builds * Update src/main.ts --------- Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/main.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index 9063ac0ec5b..7e4943bdca5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -45,8 +45,10 @@ Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems")); // biome-ignore lint/suspicious/noImplicitAnyLet: TODO let game; +// biome-ignore lint/suspicious/noImplicitAnyLet: TODO +let manifest; -const startGame = async (manifest?: any) => { +const startGame = async () => { await initI18n(); const LoadingScene = (await import("./loading-scene")).LoadingScene; const BattleScene = (await import("./battle-scene")).BattleScene; @@ -110,10 +112,13 @@ const startGame = async (manifest?: any) => { fetch("/manifest.json") .then(res => res.json()) .then(jsonResponse => { - startGame(jsonResponse.manifest); + manifest = jsonResponse.manifest; }) - .catch(() => { - // Manifest not found (likely local build) + .catch(err => { + // Manifest not found (likely local build or path error on live) + console.log(`Manifest not found. ${err}`); + }) + .finally(() => { startGame(); }); From 6937effa16ab93965e3db7892f7206989a481938 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 22 Jul 2025 13:28:10 -0400 Subject: [PATCH 2/3] [Misc/Docs] Assorted code cleanups + doc updates (#5745) * Squashed changes into 1 commit, reverted unneeded stuff * Update ability-class.ts comments * Update move.ts comments * Fixed flaky test * Applied PR reviews * Update move.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fixed ab code * Added comment for BattlerIndex * ddd * ren biome * Update battler-tags.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update battler-tags.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fixed the things * Fixed up a few `default` stuff and random enum stuff * Update move.ts comments * Revert change to pokemon.ts * Update battle-scene.ts * fixed import oopsie * Update src/field/pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/abilities/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/abilities/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fix incorrect TSDoc * Update ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 49 ++- src/data/abilities/ability.ts | 214 +++++++++--- src/data/abilities/apply-ab-attrs.ts | 18 +- src/data/arena-tag.ts | 13 +- src/data/battler-tags.ts | 9 +- src/data/moves/move.ts | 156 +++++---- src/data/moves/pokemon-move.ts | 10 +- .../utils/encounter-pokemon-utils.ts | 4 +- src/enums/MoveFlags.ts | 4 + src/enums/battle-style.ts | 12 +- src/enums/battler-index.ts | 4 + src/enums/exp-gains-speed.ts | 13 +- src/enums/exp-notification.ts | 16 +- src/field/pokemon.ts | 321 ++++++++++-------- src/modifier/modifier.ts | 25 +- src/phase-manager.ts | 9 +- src/phases/command-phase.ts | 2 +- src/phases/select-starter-phase.ts | 2 +- src/phases/title-phase.ts | 3 +- src/system/game-data.ts | 3 +- 20 files changed, 527 insertions(+), 360 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index ad0c9d84aba..bb28fb0d5b6 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -50,7 +50,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; import { EaseType } from "#enums/ease-type"; 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 { GameModes } from "#enums/game-modes"; import { ModifierPoolType } from "#enums/modifier-pool-type"; @@ -197,6 +197,7 @@ export class BattleScene extends SceneBase { public enableMoveInfo = true; public enableRetries = 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 * - 0 = 'Off' @@ -214,7 +215,7 @@ export class BattleScene extends SceneBase { public uiTheme: UiTheme = UiTheme.DEFAULT; public windowType = 0; public experimentalSprites = false; - public musicPreference: number = MusicPreference.ALLGENS; + public musicPreference: MusicPreference = MusicPreference.ALLGENS; public moveAnimations = true; public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT; public skipSeenDialogues = false; @@ -225,33 +226,18 @@ export class BattleScene extends SceneBase { * - 2 = Always (automatically skip animation when hatching 2 or more eggs) */ public eggSkipPreference = 0; - /** - * Defines the experience gain display mode. - * - * @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. + * Defines the {@linkcode ExpNotification | Experience gain display mode}. + * @defaultValue {@linkcode ExpNotification.DEFAULT} */ - public expParty: ExpNotification = 0; + public expParty: ExpNotification = ExpNotification.DEFAULT; public hpBarSpeed = 0; public fusionPaletteSwaps = true; public enableTouchControls = false; public enableVibration = false; public showBgmBar = true; - - /** - * 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; - + /** Determines the selected battle style. */ + public battleStyle: BattleStyle = BattleStyle.SWITCH; /** * Defines whether or not to show type effectiveness 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. * 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. */ 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 - * @param removedPokemon {@linkcode Pokemon} the pokemon that is being removed from the field (flee, faint), moves to be redirected FROM - * @param allyPokemon {@linkcode Pokemon} the pokemon that will have the moves be redirected TO + * Attempt to redirect a move in double battles from a fainted/removed Pokemon to its ally. + * @param removedPokemon - The {@linkcode Pokemon} having been removed from the field. + * @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it */ redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { // 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 - * @param isEnemy Whether to return the enemy's modifier bar - * @returns {ModifierBar} + * @param isEnemy - Whether to return the enemy modifier bar instead of the player bar; default `false` + * @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; } @@ -1475,10 +1462,12 @@ export class BattleScene extends SceneBase { if (!waveIndex && lastBattle) { const isNewBiome = this.isNewBiome(lastBattle); + /** Whether to reset and recall pokemon */ const resetArenaState = isNewBiome || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) || this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS; + for (const enemyPokemon of this.getEnemyParty()) { enemyPokemon.destroy(); } @@ -1853,7 +1842,7 @@ export class BattleScene extends SceneBase { } resetSeed(waveIndex?: number): void { - const wave = waveIndex || this.currentBattle?.waveIndex || 0; + const wave = waveIndex ?? this.currentBattle?.waveIndex ?? 0; this.waveSeed = shiftCharCodes(this.seed, wave); Phaser.Math.RND.sow([this.waveSeed]); console.log("Wave Seed:", this.waveSeed, wave); diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index aec5f65525e..30f6f6fb8b2 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -145,76 +145,141 @@ export class Ability implements Localizable { return this.attrs.some(attr => attr instanceof targetAttr); } - attr>(AttrType: T, ...args: ConstructorParameters): 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>(AttrType: T, ...args: ConstructorParameters): this { const attr = new AttrType(...args); this.attrs.push(attr); 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>( condition: AbAttrCondition, - AttrType: T, + attrType: T, ...args: ConstructorParameters - ): Ability { - const attr = new AttrType(...args); + ): this { + const attr = new attrType(...args); attr.addCondition(condition); this.attrs.push(attr); 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; 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; 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; 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; 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; 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); 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 { this.nameAppend += " (P)"; return this; } + /** + * Mark an ability as unimplemented. + * Unimplemented abilities are ones which have _none_ of their basic functionality enabled. + * @returns `this` + */ unimplemented(): this { this.nameAppend += " (N)"; return this; } /** - * Internal flag used for developers to document edge cases. When using this, please be sure to document the edge case. - * @returns the ability + * Mark an ability as having one or more edge cases. + * 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 { 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 { /** The pokemon that has the ability being applied */ readonly pokemon: Pokemon; @@ -245,9 +310,20 @@ export interface AbAttrParamsWithCancel extends AbAttrBaseParams { 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 { - 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. @@ -275,21 +351,43 @@ export abstract class AbAttr { } /** - * Apply ability effects without checking conditions. - * **Never call this method directly, use {@linkcode applyAbAttrs} instead.** + * Apply this attribute's effects without checking conditions. + * + * @remarks + * **Never call this method directly!** \ + * Use {@linkcode applyAbAttrs} instead. */ 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[0]>, _abilityName: string): string | 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[0]>): boolean { 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 { return this.extraCondition || null; } @@ -593,7 +691,7 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr { private immuneType: PokemonType | 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) { super(true); @@ -1526,6 +1624,11 @@ export interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams { export class FieldMultiplyStatAbAttr extends AbAttr { private stat: Stat; 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; 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 { 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: * - 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 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 Terastallized and using Tera Blast + * - The user is not a Terastallized Terapagos using Stellar-type Tera Starstorm */ override canApply({ pokemon, opponent: target, move }: MoveTypeChangeAbAttrParams): boolean { return ( (!this.condition || this.condition(pokemon, target, move)) && !noAbilityTypeOverrideMoves.has(move.id) && - (!pokemon.isTerastallized || - (move.id !== MoveId.TERA_BLAST && - (move.id !== MoveId.TERA_STARSTORM || - pokemon.getTeraType() !== PokemonType.STELLAR || - !pokemon.hasSpecies(SpeciesId.TERAPAGOS)))) + !( + pokemon.isTerastallized && + (move.id === MoveId.TERA_BLAST || + (move.id === MoveId.TERA_STARSTORM && + pokemon.getTeraType() === PokemonType.STELLAR && + pokemon.hasSpecies(SpeciesId.TERAPAGOS))) + ) ); } @@ -1661,23 +1766,19 @@ export interface AddSecondStrikeAbAttrParams extends Omit): boolean { return this.attackCondition(pokemon, opponent, move); @@ -3511,18 +3613,18 @@ export interface ConfusionOnStatusEffectAbAttrParams extends AbAttrBaseParams { */ export class ConfusionOnStatusEffectAbAttr extends AbAttr { /** List of effects to apply confusion after */ - private effects: StatusEffect[]; + private effects: ReadonlySet; constructor(...effects: StatusEffect[]) { super(); - this.effects = effects; + this.effects = new Set(effects); } /** * @returns Whether the ability can apply confusion to the opponent */ 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 - * @param allyTarget Whether to target ally, defaults to false (self-target) + * After the turn ends, resets the status of either the user or their ally. + * @param allyTarget Whether to target the user's ally; default `false` (self-target) * * @sealed */ @@ -4786,17 +4888,22 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { !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 { if (simulated) { return; } + for (const opp of pokemon.getOpponents()) { - if ( - (opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(AbilityId.COMATOSE)) && - !opp.hasAbilityWithAttr("BlockNonDirectDamageAbAttr") && - !opp.switchOutStatus - ) { + if ((opp.status?.effect !== StatusEffect.SLEEP && !opp.hasAbility(AbilityId.COMATOSE)) || opp.switchOutStatus) { + continue; + } + + const cancelled = new BooleanHolder(false); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, simulated, cancelled }); + + if (!cancelled.value) { opp.damageAndUpdate(toDmgValue(opp.getMaxHp() / 8), { result: HitResult.INDIRECT }); globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) }), @@ -4809,7 +4916,8 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { /** * Grabs the last failed Pokeball used * @sealed - * @see {@linkcode applyPostTurn} */ + * @see {@linkcode applyPostTurn} + */ export class FetchBallAbAttr extends PostTurnAbAttr { override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean { 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} * moves as if the user had used {@linkcode MoveId.MAGIC_COAT | Magic Coat}. * @sealed + * @todo Make reflection a part of this ability's effects */ export class ReflectStatusMoveAbAttr extends AbAttr { private declare readonly _: never; } +// TODO: Make these ability attributes be flags instead of dummy attributes /** @sealed */ export class NoTransformAbilityAbAttr extends AbAttr { private declare readonly _: never; diff --git a/src/data/abilities/apply-ab-attrs.ts b/src/data/abilities/apply-ab-attrs.ts index e8f01a4bb36..58f63c5924a 100644 --- a/src/data/abilities/apply-ab-attrs.ts +++ b/src/data/abilities/apply-ab-attrs.ts @@ -8,23 +8,18 @@ function applySingleAbAttrs( messages: string[] = [], ) { 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; } const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); - if ( - gainedMidTurn && - ability.getAttrs(attrType).some(attr => { - attr.is("PostSummonAbAttr") && !attr.shouldActivateOnGain(); - }) - ) { + const attrs = ability.getAttrs(attrType); + if (gainedMidTurn && attrs.some(attr => attr.is("PostSummonAbAttr") && !attr.shouldActivateOnGain())) { return; } - for (const attr of ability.getAttrs(attrType)) { + for (const attr of attrs) { 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 // 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 @@ -33,7 +28,7 @@ function applySingleAbAttrs( continue; } - globalScene.phaseManager.setPhaseQueueSplice(); + let abShown = false; if (attr.showAbility && !simulated) { globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); @@ -45,6 +40,7 @@ function applySingleAbAttrs( if (!simulated) { globalScene.phaseManager.queueMessage(message); } + // TODO: Should messages be added to the array if they aren't actually shown? messages.push(message); } // The `as any` cast here uses the same reasoning as above. @@ -57,8 +53,6 @@ function applySingleAbAttrs( if (!simulated) { pokemon.waveData.abilitiesApplied.add(ability.id); } - - globalScene.phaseManager.clearPhaseQueueSplice(); } } diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 0c369f8b22d..ab50d279cc5 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -22,6 +22,8 @@ import type { Pokemon } from "#field/pokemon"; import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common"; import i18next from "i18next"; +// TODO: Add a class for tags that explicitly REQUIRE a source move (as currently we have a lot of bangs) + export abstract class ArenaTag { constructor( public tagType: ArenaTagType, @@ -50,8 +52,17 @@ export abstract class ArenaTag { 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 { - 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 { diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 72fa6eaa64b..060b17e889b 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -74,10 +74,13 @@ export class BattlerTag { /** * 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 { - // TODO: Maybe flip this (return `true` if tag needs removal) 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 - * 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 * match a condition. A restricted move gets cancelled before it is used. diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index c4b4b6e844e..d8863016002 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -166,9 +166,9 @@ export abstract class Move implements Localizable { } /** - * Get all move attributes that match `attrType` - * @param attrType any attribute that extends {@linkcode MoveAttr} - * @returns Array of attributes that match `attrType`, Empty Array if none match. + * Get all move attributes that match `attrType`. + * @param attrType - The name of a {@linkcode MoveAttr} to search for + * @returns An array containing all attributes matching `attrType`, or an empty array if none match. */ getAttrs(attrType: T): (MoveAttrMap[T])[] { const targetAttr = MoveAttrs[attrType]; @@ -179,9 +179,9 @@ export abstract class Move implements Localizable { } /** - * Check if a move has an attribute that matches `attrType` - * @param attrType any attribute that extends {@linkcode MoveAttr} - * @returns true if the move has attribute `attrType` + * Check if a move has an attribute that matches `attrType`. + * @param attrType - The name of a {@linkcode MoveAttr} to search for + * @returns Whether this move has at least 1 attribute that matches `attrType` */ hasAttr(attrType: MoveAttrString): boolean { const targetAttr = MoveAttrs[attrType]; @@ -193,23 +193,25 @@ export abstract class Move implements Localizable { } /** - * Takes as input a boolean function and returns the first MoveAttr in attrs that matches true - * @param attrPredicate - * @returns the first {@linkcode MoveAttr} element in attrs that makes the input function return true + * Find the first attribute that matches a given predicate function. + * @param attrPredicate - The predicate function to search `MoveAttr`s by + * @returns The first {@linkcode MoveAttr} for which `attrPredicate` returns `true` */ 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) - * if the MoveAttr also comes with a condition, also adds that to the conditions array: {@linkcode MoveCondition} - * @param AttrType {@linkcode MoveAttr} the constructor of a MoveAttr class - * @param args the args needed to instantiate a the given class - * @returns the called object {@linkcode Move} + * Adds a new MoveAttr to this move (appends to the attr array). + * If the MoveAttr also comes with a condition, it is added to its {@linkcode MoveCondition} array. + * @param attrType - The {@linkcode MoveAttr} to add + * @param args - The arguments needed to instantiate the given class + * @returns `this` */ - attr>(AttrType: T, ...args: ConstructorParameters): this { - const attr = new AttrType(...args); + attr>(attrType: T, ...args: ConstructorParameters): this { + const attr = new attrType(...args); this.attrs.push(attr); let attrCondition = attr.getCondition(); if (attrCondition) { @@ -223,11 +225,13 @@ export abstract class Move implements Localizable { } /** - * Adds a new MoveAttr to the move (appends to the attr array) - * if the MoveAttr also comes with a condition, also adds that to the conditions array: {@linkcode MoveCondition} - * 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 - * @returns the called object {@linkcode Move} + * Adds a new MoveAttr to this move (appends to the attr array). + * If the MoveAttr also comes with a condition, it is added to its {@linkcode MoveCondition} array. + * + * Similar to {@linkcode attr}, except this takes an already instantiated {@linkcode MoveAttr} object + * as opposed to a constructor and its arguments. + * @param attrAdd - The {@linkcode MoveAttr} to add + * @returns `this` */ addAttr(attrAdd: MoveAttr): this { this.attrs.push(attrAdd); @@ -244,8 +248,8 @@ export abstract class Move implements Localizable { /** * Sets the move target of this move - * @param moveTarget {@linkcode MoveTarget} the move target to set - * @returns the called object {@linkcode Move} + * @param moveTarget - The {@linkcode MoveTarget} to set + * @returns `this` */ target(moveTarget: MoveTarget): this { this.moveTarget = moveTarget; @@ -253,13 +257,13 @@ export abstract class Move implements Localizable { } /** - * Getter function that returns if this Move has a MoveFlag - * @param flag {@linkcode MoveFlags} to check - * @returns boolean + * Getter function that returns if this Move has a given MoveFlag. + * @param flag - The {@linkcode MoveFlags} to check + * @returns Whether this Move has the specified flag. */ 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 - return !!(this.flags & flag); + // Flags are internally represented as bitmasks, so we check by taking the bitwise AND. + return (this.flags & flag) !== MoveFlags.NONE; } /** @@ -304,13 +308,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. - * @param user - The source of this move - * @param target - The target of this move - * @param type - The type of the move's target - * @returns boolean + * @param user - The {@linkcode Pokemon} using this move + * @param target - The {@linkcode Pokemon} targeted by this move + * @param type - The {@linkcode PokemonType} of the target + * @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 { if (this.moveTarget === MoveTarget.USER) { @@ -324,7 +328,7 @@ export abstract class Move implements Localizable { } break; 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; } break; @@ -334,9 +338,9 @@ export abstract class Move implements Localizable { /** * Checks if the move would hit its target's Substitute instead of the target itself. - * @param user The {@linkcode Pokemon} using this move - * @param target The {@linkcode Pokemon} targeted by this move - * @returns `true` if the move can bypass the target's Substitute; `false` otherwise. + * @param user - The {@linkcode Pokemon} using this move + * @param target - The {@linkcode Pokemon} targeted by this move + * @returns Whether this Move will hit the target's Substitute (assuming one exists). */ hitsSubstitute(user: Pokemon, target?: Pokemon): boolean { if ([ MoveTarget.USER, MoveTarget.USER_SIDE, MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.moveTarget) @@ -354,13 +358,14 @@ export abstract class Move implements Localizable { } /** - * Adds a move condition to the move - * @param condition {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}, appends to conditions array a new MoveCondition object - * @returns the called object {@linkcode Move} + * Adds a condition to this move (in addition to any provided by its prior {@linkcode MoveAttr}s). + * The move will fail upon use if at least 1 of its conditions is not met. + * @param condition - The {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} to add to the conditions array. + * @returns `this` */ condition(condition: MoveCondition | MoveConditionFunc): this { if (typeof condition === "function") { - condition = new MoveCondition(condition as MoveConditionFunc); + condition = new MoveCondition(condition); } this.conditions.push(condition); @@ -368,16 +373,22 @@ export abstract class Move implements Localizable { } /** - * Internal dev flag for documenting edge cases. When using this, please document the known edge case. - * @returns the called object {@linkcode Move} + * Mark a move as having one or more edge cases. + * 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 { return this; } /** - * Marks the move as "partial": appends texts to the move name - * @returns the called object {@linkcode Move} + * Mark this move as partially implemented. + * 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 { this.nameAppend += " (P)"; @@ -385,8 +396,10 @@ export abstract class Move implements Localizable { } /** - * Marks the move as "unimplemented": appends texts to the move name - * @returns the called object {@linkcode Move} + * Mark this move as unimplemented. + * Unimplemented moves are ones which have _none_ of their basic functionality enabled, + * and cannot be used. + * @returns `this` */ unimplemented(): this { this.nameAppend += " (N)"; @@ -961,10 +974,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) { super(id, type, category, MoveTarget.NEAR_OTHER, power, accuracy, pp, chance, priority, generation); - /** - * {@link 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; - */ + // > All damaging Fire-type moves can... thaw a frozen target, regardless of whether or not they have a chance to burn. + // - https://bulbapedia.bulbagarden.net/wiki/Freeze_(status_condition) if (this.type === PokemonType.FIRE) { this.addAttr(new HealStatusEffectAttr(false, StatusEffect.FREEZE)); } @@ -1220,7 +1231,8 @@ interface MoveEffectAttrOptions { effectChanceOverride?: number; } -/** Base class defining all Move Effect Attributes +/** + * Base class defining all Move Effect Attributes * @extends MoveAttr * @see {@linkcode apply} */ @@ -1238,8 +1250,7 @@ export class MoveEffectAttr extends MoveAttr { /** * Defines when this effect should trigger in the move's effect order. - * @default MoveEffectTrigger.POST_APPLY - * @see {@linkcode MoveEffectTrigger} + * @defaultValue {@linkcode MoveEffectTrigger.POST_APPLY} */ public get trigger () { return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY; @@ -1248,7 +1259,7 @@ export class MoveEffectAttr extends MoveAttr { /** * `true` if this effect should only trigger on the first hit of * multi-hit moves. - * @default false + * @defaultValue `false` */ public get firstHitOnly () { return this.options?.firstHitOnly ?? false; @@ -1257,7 +1268,7 @@ export class MoveEffectAttr extends MoveAttr { /** * `true` if this effect should only trigger on the last hit of * multi-hit moves. - * @default false + * @defaultValue `false` */ public get lastHitOnly () { return this.options?.lastHitOnly ?? false; @@ -1266,7 +1277,7 @@ export class MoveEffectAttr extends MoveAttr { /** * `true` if this effect should apply only upon hitting a target * for the first time when targeting multiple {@linkcode Pokemon}. - * @default false + * @defaultValue `false` */ public get firstTargetOnly () { return this.options?.firstTargetOnly ?? false; @@ -2570,9 +2581,12 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { } /** - * Applies the effect of 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. - * @returns `true` if Psycho Shift's effect is able to be applied to the 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. + * @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 { const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined); @@ -2905,6 +2919,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 { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (user.status?.effect === StatusEffect.SLEEP) { @@ -2922,7 +2942,7 @@ export class BypassSleepAttr extends MoveAttr { * @param move */ 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; } } @@ -3253,7 +3273,7 @@ export class StatStageChangeAttr extends MoveEffectAttr { /** * `true` to display a message for the stat change. - * @default true + * @defaultValue `true` */ private get showMessage () { return this.options?.showMessage ?? true; @@ -5416,7 +5436,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); applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled}); if (cancelled.value) { @@ -7055,7 +7078,8 @@ export class CopyMoveAttr extends CallMoveAttr { /** * 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 { private movesetMove: PokemonMove; diff --git a/src/data/moves/pokemon-move.ts b/src/data/moves/pokemon-move.ts index ec5ae195bd6..d3f68fe9db4 100644 --- a/src/data/moves/pokemon-move.ts +++ b/src/data/moves/pokemon-move.ts @@ -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. * - * @param pokemon - {@linkcode Pokemon} that would be using this move - * @param ignorePp - If `true`, skips the PP check - * @param ignoreRestrictionTags - If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag}) - * @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`. + * @param pokemon - The {@linkcode Pokemon} attempting to use this move + * @param ignorePp - Whether to ignore checking if the move is out of PP; default `false` + * @param ignoreRestrictionTags - Whether to skip checks for {@linkcode MoveRestrictionBattlerTag}s; default `false` + * @returns Whether this {@linkcode PokemonMove} can be selected by this Pokemon. */ isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean { // TODO: Add Sky Drop's 1 turn stall diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index e4211baa78a..19f06707257 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -406,12 +406,12 @@ export async function applyModifierTypeToPlayerPokemon( // Check if the Pokemon has max stacks of that item already const modifier = modType.newModifier(pokemon); const existing = globalScene.findModifier( - m => + (m): m is PokemonHeldItemModifier => m instanceof PokemonHeldItemModifier && m.type.id === modType.id && m.pokemonId === pokemon.id && m.matchType(modifier), - ) as PokemonHeldItemModifier; + ) as PokemonHeldItemModifier | undefined; // At max stacks if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { diff --git a/src/enums/MoveFlags.ts b/src/enums/MoveFlags.ts index 1155417da6d..06de265df09 100644 --- a/src/enums/MoveFlags.ts +++ b/src/enums/MoveFlags.ts @@ -1,3 +1,7 @@ +/** + * A list of possible flags that various moves may have. + * Represented internally as a bitmask. + */ export enum MoveFlags { NONE = 0, MAKES_CONTACT = 1 << 0, diff --git a/src/enums/battle-style.ts b/src/enums/battle-style.ts index 3f3e7f6d8f6..2fba10f1bf9 100644 --- a/src/enums/battle-style.ts +++ b/src/enums/battle-style.ts @@ -1,9 +1,7 @@ -/** - * 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. -*/ +/** Enum for selected battle style. */ export enum BattleStyle { - SWITCH, - SET + /** Display option to switch active pokemon at battle start. */ + SWITCH, + /** Hide option to switch active pokemon at battle start. */ + SET } diff --git a/src/enums/battler-index.ts b/src/enums/battler-index.ts index 32b1684c86c..253e5bfc3ed 100644 --- a/src/enums/battler-index.ts +++ b/src/enums/battler-index.ts @@ -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 { ATTACKER = -1, PLAYER, diff --git a/src/enums/exp-gains-speed.ts b/src/enums/exp-gains-speed.ts index 964c4f99c70..f5f36a1c78d 100644 --- a/src/enums/exp-gains-speed.ts +++ b/src/enums/exp-gains-speed.ts @@ -1,15 +1,4 @@ -/** - * 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. - */ +/** Defines the speed of gaining experience. */ export enum ExpGainsSpeed { /** The normal speed. */ DEFAULT, diff --git a/src/enums/exp-notification.ts b/src/enums/exp-notification.ts index b7f50814d3a..a64d8591e76 100644 --- a/src/enums/exp-notification.ts +++ b/src/enums/exp-notification.ts @@ -1,11 +1,9 @@ -/** - * 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 -*/ +/** Enum for party experience gain notification style. */ export enum ExpNotification { - DEFAULT, - ONLY_LEVEL_UP, - SKIP + /** Display amount flyout for all off-field party members upon gaining any amount of EXP. */ + DEFAULT, + /** 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 } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6ccccec7d91..34d8c2b365a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -375,7 +375,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.ivs = ivs || getIvsFromId(this.id); if (this.gender === undefined) { - this.generateGender(); + this.gender = this.species.generateGender(); } 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) { 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; try { if (nickname) { - return decodeURIComponent(escape(atob(nickname))); + return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually... } return name; } catch (err) { @@ -455,11 +457,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - getPokeball(useIllusion = false) { - if (useIllusion) { - return this.summonData.illusion?.pokeball ?? this.pokeball; - } - return this.pokeball; + /** + * Return this Pokemon's {@linkcode PokeballType}. + * @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. + */ + getPokeball(useIllusion = false): PokeballType { + return useIllusion && this.summonData.illusion ? this.summonData.illusion.pokeball : this.pokeball; } init(): void { @@ -516,17 +520,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Checks if a pokemon is fainted (ie: its `hp <= 0`). - * It's usually better to call {@linkcode isAllowedInBattle()} - * @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT} - * @returns `true` if the pokemon is fainted + * Usually should not be called directly in favor of calling {@linkcode isAllowedInBattle()}. + * @param checkStatus - Whether to also check that the pokemon's status is {@linkcode StatusEffect.FAINT}; default `false` + * @returns Whether this Pokemon is fainted, as described above. */ public isFainted(checkStatus = false): boolean { 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. - * @returns {boolean} `true` if pokemon is allowed in battle + * Check if this pokemon is both not fainted and allowed to be used based on currently active challenges. + * @returns Whether this Pokemon is allowed to partake in battle. */ public isAllowedInBattle(): boolean { 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. - * It's usually better to call {@linkcode isAllowedInBattle()} - * @returns {boolean} `true` if pokemon is allowed in battle + * Usually should not be called directly in favor of consulting {@linkcode isAllowedInBattle()}. + * @returns Whether this Pokemon is allowed under the current challenge conditions. */ public isAllowedInChallenge(): boolean { 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). - * @param onField `true` to also check if the pokemon is currently on the field; default `false` - * @returns `true` if the pokemon is "active", as described above. + * @param onField - Whether to also check if the pokemon is currently on the field; default `false` + * @returns Whether this pokemon is considered "active", as described above. * Returns `false` if there is no active {@linkcode BattleScene} or the pokemon is disallowed. */ public isActive(onField = false): boolean { @@ -703,7 +707,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { 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 { /** 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. - * - * @param cacheKey the cache key for the variant color sprite - * @param useExpSprite should the experimental sprite be used - * @param battleSpritePath the filename of the sprite + * Attempt to process variant sprite color caches. + * @param cacheKey - the cache key for the variant color sprite + * @param useExpSprite - Whether experimental sprites should be used if present + * @param battleSpritePath - the filename of the sprite */ async populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { 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; } - 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, "/"); + 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}. - * @param ignoreOverride - Whether to ignore overridden species from {@linkcode MoveId.TRANSFORM}, default `false`. - * This overrides `useIllusion` if `true`. - * @param useIllusion - `true` to use the speciesForm of the illusion; default `false`. + * Return this Pokemon's {@linkcode PokemonSpeciesForm | SpeciesForm}. + * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false` + * and overrides `useIllusion`. + * @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 { 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 = useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!; 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 * items, move effects, opponent abilities, and whether there was a critical * hit. - * @param stat the desired {@linkcode EffectiveStat} - * @param opponent the target {@linkcode Pokemon} - * @param move the {@linkcode Move} being used - * @param ignoreAbility determines whether this Pokemon's abilities should be ignored during the stat calculation - * @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation. - * @param ignoreAllyAbility during an attack, determines whether the ally Pokemon's abilities should be ignored during the stat calculation. - * @param isCritical determines whether a critical hit has occurred or not (`false` by default) - * @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering - * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` - * @returns the final in-battle value of a stat + * @param stat - The desired {@linkcode EffectiveStat | Stat} to check. + * @param opponent - The {@linkcode Pokemon} being targeted, if applicable. + * @param move - The {@linkcode Move} being used, if any. Used to check ability ignoring effects and similar. + * @param ignoreAbility - Whether to ignore ability effects of the user; default `false`. + * @param ignoreOppAbility - Whether to ignore ability effects of the target; default `false`. + * @param ignoreAllyAbility - Whether to ignore ability effects of the user's allies; default `false`. + * @param isCritical - Whether a critical hit has occurred or not; default `false`. + * If `true`, will nullify offensive stat drops or defensive stat boosts. + * @param simulated - Whether to nullify any effects that produce changes to game state during calculations; default `true` + * @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( stat: EffectiveStat, 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 const fieldApplied = new BooleanHolder(false); for (const pokemon of globalScene.getField(true)) { + // TODO: remove `canStack` toggle from ability as breaking out renders it useless applyAbAttrs("FieldMultiplyStatAbAttr", { pokemon, stat, @@ -1448,6 +1465,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { break; } } + if (!ignoreAbility) { applyAbAttrs("StatMultiplierAbAttr", { 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) { 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) { 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 { if (!useIllusion && this.summonData.illusion) { - return !!( + return ( 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); } @@ -1700,9 +1728,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * - * @param useIllusion - Whether we want the fake or real shininess (illusion ability). - * @returns `true` if the {@linkcode Pokemon} is shiny and the fusion is shiny as well, `false` otherwise + * Check whether this Pokemon is doubly shiny (both normal and fusion are shiny). + * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @returns Whether this pokemon's base and fusion counterparts are both shiny. */ isDoubleShiny(useIllusion = false): boolean { if (!useIllusion && this.summonData.illusion?.basePokemon) { @@ -1712,11 +1740,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.summonData.illusion.basePokemon.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 { if (!useIllusion && this.summonData.illusion) { @@ -1724,9 +1756,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ? this.summonData.illusion.basePokemon!.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 { if (doubleShiny) { 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 pokemon's overall luck value, based on its shininess (1 pt per variant lvl). + * @returns The luck value of this Pokemon. + */ getLuck(): number { 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 { - if (useIllusion && this.summonData.illusion) { - return !!this.summonData.illusion.fusionSpecies; - } - return !!this.fusionSpecies; + return useIllusion && this.summonData.illusion ? !!this.summonData.illusion.fusionSpecies : !!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 { 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}. - * @param species the pokemon {@linkcode SpeciesId} to check - * @returns `true` if the {@linkcode Pokemon} has a fusion with the specified {@linkcode SpeciesId}, `false` otherwise + * Check whether this {@linkcode Pokemon} has a fusion with the specified {@linkcode SpeciesId}. + * @param species - The {@linkcode SpeciesId} to check against. + * @returns Whether this Pokemon is currently fused with the specified {@linkcode SpeciesId}. */ hasFusionSpecies(species: SpeciesId): boolean { return this.fusionSpecies?.speciesId === species; } /** - * Checks if the {@linkcode Pokemon} has is the specified {@linkcode SpeciesId} or is fused with it. - * @param species the pokemon {@linkcode SpeciesId} to check - * @param formKey If provided, requires the species to be in that form - * @returns `true` if the pokemon is the species or is fused with it, `false` otherwise + * Check whether this {@linkcode Pokemon} either is or is fused with the given {@linkcode SpeciesId}. + * @param species - The {@linkcode SpeciesId} to check against. + * @param formKey - If provided, will require the species to be in the given form. + * @returns Whether this Pokemon has this species as either its base or fusion counterpart. */ hasSpecies(species: SpeciesId, formKey?: string): boolean { if (isNullOrUndefined(formKey)) { @@ -1782,7 +1825,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { 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; // 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 - * on the species it was met at or by the first {@linkcode Pokemon} in its evolution + * Check which egg moves have been unlocked for this {@linkcode Pokemon}. + * 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. - * @returns an array of {@linkcode MoveId}, the length of which is determined by how many - * egg moves are unlocked for that species. + * @returns An array of all {@linkcode MoveId}s that are egg moves and unlocked for this Pokemon. */ getUnlockedEggMoves(): 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. * * 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. - * @returns an array of {@linkcode MoveId}, the length of which is determined - * by how many learnable moves there are for the {@linkcode Pokemon}. + * @returns An array of {@linkcode MoveId}s, as described above. */ public getLearnableLevelMoves(): MoveId[] { 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 - * @param includeTeraType - `true` to include tera-formed type; Default: `false` - * @param forDefend - `true` if the pokemon is defending from an attack; Default: `false` - * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` - * @param useIllusion - `true` to return the types of the illusion instead of the actual types; Default: `false` - * @returns array of {@linkcode PokemonType} + * Evaluate and return this Pokemon's typing. + * @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `false` + * @param forDefend - Whether this Pokemon is currently receiving an attack; default `false` + * @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 An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or percieved). */ public getTypes( includeTeraType = false, @@ -1947,7 +1994,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } // 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); if (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 - * @param type - {@linkcode PokemonType} to check - * @param includeTeraType - `true` to include tera-formed type; Default: `true` - * @param forDefend - `true` if the pokemon is defending from an attack; Default: `false` - * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` - * @returns `true` if the Pokemon's type matches + * Check if this Pokemon's typing includes the specified type. + * @param type - The {@linkcode PokemonType} to check + * @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `true` + * @param forDefend - Whether this Pokemon is currently receiving an attack; default `false` + * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false` + * @returns Whether this Pokemon is of the specified type. */ 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. - * 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. - * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` - * @returns The non-passive {@linkcode Ability} of the pokemon + * Get this Pokemon's non-passive {@linkcode Ability}, factoring in fusions, overrides and ability-changing effects. + + * Should rarely be called directly in favor of {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr}, + * both of which check both ability slots and account for suppression. + * @see {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} are the intended ways to check abilities in most cases + * @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 { if (!ignoreOverride && this.summonData.ability) { @@ -2131,7 +2179,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (passive && !this.hasPassive()) { return false; } - const ability = !passive ? this.getAbility() : this.getPassiveAbility(); + const ability = passive ? this.getPassiveAbility() : this.getAbility(); if (this.isFusion() && ability.hasAttr("NoFusionAbilityAbAttr")) { return false; } @@ -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 - * effects which can affect whether an ability will be present or in effect, and both passive and - * non-passive. This is the primary way to check whether a pokemon has a particular ability. - * @param ability The ability to check for + * Check whether a pokemon has the specified ability in effect, either as a normal or passive ability. + * Accounts for all the various effects which can disable or modify abilities. + * @param ability - The {@linkcode Abilities | Ability} to check for * @param canApply - Whether to check if the ability is currently active; default `true` - * @param ignoreOverride Whether to ignore ability changing effects; default `false` - * @returns `true` if the ability is present and active + * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false` + * @returns Whether this {@linkcode Pokemon} has the given ability */ public hasAbility(ability: AbilityId, canApply = true, ignoreOverride = false): boolean { 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. - * Accounts for all the various effects which can affect whether an ability will be present or - * in effect, and both passive and non-passive. This is one of the two primary ways to check - * whether a pokemon has a particular ability. - * @param attrType The {@link AbAttr | ability attribute} to check for + * 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 disable or modify abilities. + * @param attrType - The {@linkcode AbAttr | attribute} to check for * @param canApply - Whether to check if the ability is currently active; default `true` - * @param ignoreOverride Whether to ignore ability changing effects; default `false` - * @returns `true` if an ability with the given {@linkcode AbAttr} is present and active + * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false` + * @returns Whether this Pokemon has an ability with the given {@linkcode AbAttr}. */ public hasAbilityWithAttr(attrType: AbAttrString, canApply = true, ignoreOverride = false): boolean { 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); let weightRemoved = 0; if (!isNullOrUndefined(autotomizedTag)) { - weightRemoved = 100 * autotomizedTag!.autotomizeCount; + weightRemoved = 100 * autotomizedTag.autotomizeCount; } const minWeight = 0.1; 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 * @see {@linkcode getAccuracyMultiplier} - * @param stat the desired {@linkcode EffectiveStat} - * @param opponent the target {@linkcode Pokemon} - * @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 stat - The {@linkcode EffectiveStat} to calculate + * @param opponent - The {@linkcode Pokemon} being targeted + * @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 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 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 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[] { 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. - * The retrieved move entries are sorted in order from NEWEST to OLDEST. - * @param moveCount The number of move entries to retrieve. - * If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}). - * Default is `1`. - * @returns A list of {@linkcode TurnMove}, as specified above. + * 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**. + * @param moveCount - The maximum number of move entries to retrieve. + * If negative, retrieves the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}). + * Default is `1`. + * @returns An array of {@linkcode TurnMove}, as specified above. */ + // TODO: Update documentation in dancer PR to mention "getLastNonVirtualMove" getLastXMoves(moveCount = 1): TurnMove[] { const moveHistory = this.getMoveHistory(); - if (moveCount >= 0) { + if (moveCount > 0) { 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. - * @param heldItem The item stack to be reduced by 1. - * @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`. - * @returns `true` if the item was removed successfully, `false` otherwise. + * @param heldItem - The item stack to be reduced. + * @param forBattle - Whether to trigger in-battle effects (such as Unburden) after losing the item. Default: `true` + * 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 { + // TODO: What does a -1 pokemon id mean? if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) { 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 - * or through the the Scene.getEncounterBossSegments function + * Set this {@linkcode EnemyPokemon}'s boss status. * - * @param boss if the pokemon is a boss - * @param bossSegments amount of boss segments (health-bar segments) + * @param boss - Whether this pokemon should be a boss; default `true` + * @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 { - if (boss) { - this.bossSegments = - bossSegments || - globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, this.level, this.species, true); - this.bossSegmentIndex = this.bossSegments - 1; - } else { + setBoss(boss = true, bossSegments?: number): void { + if (!boss) { this.bossSegments = 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 { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 79bd849e5ba..f8c35b3e8f9 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -52,25 +52,11 @@ const iconOverflowIndex = 24; export const modifierSortFunc = (a: Modifier, b: Modifier): number => { const itemNameMatch = a.type.name.localeCompare(b.type.name); const typeNameMatch = a.constructor.name.localeCompare(b.constructor.name); - const aId = a instanceof PokemonHeldItemModifier && a.pokemonId ? a.pokemonId : 4294967295; - const bId = b instanceof PokemonHeldItemModifier && b.pokemonId ? b.pokemonId : 4294967295; + const aId = a instanceof PokemonHeldItemModifier ? a.pokemonId : -1; + const bId = b instanceof PokemonHeldItemModifier ? b.pokemonId : -1; - //First sort by pokemonID - if (aId < bId) { - 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; + // First sort by pokemon ID, then by item type and then name + return aId - bId || typeNameMatch || itemNameMatch; }; export class ModifierBar extends Phaser.GameObjects.Container { @@ -757,7 +743,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { return 1; } - getMaxStackCount(forThreshold?: boolean): number { + getMaxStackCount(forThreshold = false): number { const pokemon = this.getPokemon(); if (!pokemon) { return 0; @@ -2814,6 +2800,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { damageMultiplier.value *= 1 - 0.25 * this.getStackCount(); return true; } + if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== this.getStackCount() + 1) { // Deal 25% damage for each remaining Multi Lens hit damageMultiplier.value *= 0.25; diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 7c1f2986593..73ea373904f 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -560,14 +560,13 @@ export class PhaseManager { } /** - * Queues an ability bar flyout phase - * @param pokemon The pokemon who has the ability - * @param passive Whether the ability is a passive - * @param show Whether to show or hide the bar + * Queue a phase to show or hide the ability flyout bar. + * @param pokemon - The {@linkcode Pokemon} whose ability is being activated + * @param passive - Whether the ability is a passive + * @param show - Whether to show or hide the bar */ public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void { this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase()); - this.clearPhaseQueueSplice(); } /** diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index b90c5a6b00a..14674037fbe 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -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. - const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag; + const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag | undefined; if (encoreTag) { this.getPokemon().lapseTag(BattlerTagType.ENCORE); } diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index f8f4eeee4d2..af056ebb4ee 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -37,7 +37,7 @@ export class SelectStarterPhase extends Phase { /** * 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[]) { const party = globalScene.getPlayerParty(); diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 42dd761de27..38e3ff3a017 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -204,7 +204,7 @@ export class TitlePhase extends Phase { globalScene.eventManager.startEventChallenges(); globalScene.setSeed(seed); - globalScene.resetSeed(0); + globalScene.resetSeed(); globalScene.money = globalScene.gameMode.getStartingMoney(); @@ -283,6 +283,7 @@ export class TitlePhase extends Phase { console.error("Failed to load daily run:\n", err); }); } 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)); if (!isNullOrUndefined(Overrides.DAILY_RUN_SEED_OVERRIDE)) { seed = Overrides.DAILY_RUN_SEED_OVERRIDE; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 6abb5518d1c..62603260cb2 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1247,7 +1247,8 @@ export class GameData { // (or prevent them from being null) // If the value is able to *not exist*, it should say so in the code 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) { case "party": case "enemyParty": { From 069e8a47d62ede6d5c7b687581a68c1a8add6b0a Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:26:27 -0600 Subject: [PATCH 3/3] [Bug][Refactor] Fix loading arena tags (#6110) * Improve type safety; add missing loadTag overrides to wish and neutralizing gas * More automatic type safety for arena tags * Fixup wording of lead comment in arena-tag.ts * Apply kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Allow abstract constructors for arena methods * Address dean's comments from code review * Add missing newline after interface definition Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Format with biome * Update tsdoc of ConditionalProtectTag Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Apply kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/@types/arena-tags.ts | 48 +++ src/@types/type-helpers.ts | 31 ++ src/data/arena-tag.ts | 578 ++++++++++++++++++++++-------------- src/data/terrain.ts | 5 + src/data/weather.ts | 5 + src/enums/arena-tag-type.ts | 10 + src/field/arena.ts | 16 +- src/field/pokemon.ts | 2 +- src/system/arena-data.ts | 48 +-- src/system/game-data.ts | 4 +- 10 files changed, 503 insertions(+), 244 deletions(-) create mode 100644 src/@types/arena-tags.ts diff --git a/src/@types/arena-tags.ts b/src/@types/arena-tags.ts new file mode 100644 index 00000000000..ab4339b2fef --- /dev/null +++ b/src/@types/arena-tags.ts @@ -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; + +/** + * 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; diff --git a/src/@types/type-helpers.ts b/src/@types/type-helpers.ts index 077bef62f1f..3a5c88e3f15 100644 --- a/src/@types/type-helpers.ts +++ b/src/@types/type-helpers.ts @@ -44,3 +44,34 @@ export type Mutable = { export type InferKeys, V extends EnumValues> = { [K in keyof O]: O[K] extends V ? K : never; }[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 = { + [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 = { + [K in keyof Class as Class[K] extends AnyFn ? never : K]: Class[K] extends Array + ? NonFunctionPropertiesRecursive[] + : Class[K] extends object + ? NonFunctionPropertiesRecursive + : Class[K]; +}; + +export type AbstractConstructor = abstract new (...args: any[]) => T; diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index ab50d279cc5..b25e79649a0 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -19,19 +19,88 @@ import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import type { Arena } from "#field/arena"; 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 i18next from "i18next"; -// TODO: Add a class for tags that explicitly REQUIRE a source move (as currently we have a lot of bangs) +/* +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 -export abstract class ArenaTag { - constructor( - public tagType: ArenaTagType, - public turnCount: number, - public sourceMove?: MoveId, - public sourceId?: number, - public side: ArenaTagSide = ArenaTagSide.BOTH, - ) {} +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. + * 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. + */ +export abstract class ArenaTag implements BaseArenaTag { + /** The type of the arena tag */ + public abstract readonly tagType: ArenaTagType; + public turnCount: number; + public sourceMove?: MoveId; + public sourceId: number | undefined; + 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 { return true; @@ -72,9 +141,9 @@ export abstract class ArenaTag { /** * 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 - * @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.sourceMove = source.sourceMove; this.sourceId = source.sourceId; @@ -107,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}. * Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist. */ -export class MistTag extends ArenaTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.MIST, turnCount, MoveId.MIST, sourceId, side); +export class MistTag extends SerializableArenaTag { + readonly tagType = ArenaTagType.MIST; + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.MIST, sourceId, side); } onAdd(arena: Arena, quiet = false): void { @@ -170,33 +247,11 @@ export class MistTag extends ArenaTag { /** * Reduces the damage of specific move categories in the arena. - * @extends ArenaTag */ -export class WeakenMoveScreenTag extends ArenaTag { - protected 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; - } +export abstract class WeakenMoveScreenTag extends SerializableArenaTag { + public abstract readonly tagType: ArenaScreenTagType; + // Getter to avoid unnecessary serialization and prevent modification + protected abstract get weakenedCategories(): MoveCategory[]; /** * Applies the weakening effect to the move. @@ -233,8 +288,13 @@ export class WeakenMoveScreenTag extends ArenaTag { * Used by {@linkcode MoveId.REFLECT} */ class ReflectTag extends WeakenMoveScreenTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.REFLECT, turnCount, MoveId.REFLECT, sourceId, side, [MoveCategory.PHYSICAL]); + public readonly tagType = ArenaTagType.REFLECT; + 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 { @@ -253,8 +313,12 @@ class ReflectTag extends WeakenMoveScreenTag { * Used by {@linkcode MoveId.LIGHT_SCREEN} */ class LightScreenTag extends WeakenMoveScreenTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.LIGHT_SCREEN, turnCount, MoveId.LIGHT_SCREEN, sourceId, side, [MoveCategory.SPECIAL]); + public readonly tagType = ArenaTagType.LIGHT_SCREEN; + 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 { @@ -273,11 +337,13 @@ class LightScreenTag extends WeakenMoveScreenTag { * Used by {@linkcode MoveId.AURORA_VEIL} */ class AuroraVeilTag extends WeakenMoveScreenTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.AURORA_VEIL, turnCount, MoveId.AURORA_VEIL, sourceId, side, [ - MoveCategory.SPECIAL, - MoveCategory.PHYSICAL, - ]); + public readonly tagType = ArenaTagType.AURORA_VEIL; + protected get weakenedCategories(): [MoveCategory.PHYSICAL, MoveCategory.SPECIAL] { + return [MoveCategory.PHYSICAL, MoveCategory.SPECIAL]; + } + + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.AURORA_VEIL, sourceId, side); } onAdd(_arena: Arena, quiet = false): void { @@ -297,21 +363,23 @@ type ProtectConditionFunc = (arena: Arena, moveId: MoveId) => boolean; * Class to implement conditional team protection * 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 */ 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; constructor( - tagType: ArenaTagType, sourceMove: MoveId, - sourceId: number, + sourceId: number | undefined, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass = false, ) { - super(tagType, 1, sourceMove, sourceId, side); + super(1, sourceMove, sourceId, side); this.protectConditionFunc = condition; this.ignoresBypass = ignoresBypass; @@ -397,8 +465,9 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (_arena, moveId) => { * Condition: The incoming move has increased priority. */ class QuickGuardTag extends ConditionalProtectTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.QUICK_GUARD, MoveId.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc); + public readonly tagType = ArenaTagType.QUICK_GUARD; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc); } } @@ -428,8 +497,9 @@ const WideGuardConditionFunc: ProtectConditionFunc = (_arena, moveId): boolean = * can be an ally or enemy. */ class WideGuardTag extends ConditionalProtectTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.WIDE_GUARD, MoveId.WIDE_GUARD, sourceId, side, WideGuardConditionFunc); + public readonly tagType = ArenaTagType.WIDE_GUARD; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.WIDE_GUARD, sourceId, side, WideGuardConditionFunc); } } @@ -450,8 +520,9 @@ const MatBlockConditionFunc: ProtectConditionFunc = (_arena, moveId): boolean => * Condition: The incoming move is a Physical or Special attack move. */ class MatBlockTag extends ConditionalProtectTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.MAT_BLOCK, MoveId.MAT_BLOCK, sourceId, side, MatBlockConditionFunc); + public readonly tagType = ArenaTagType.MAT_BLOCK; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.MAT_BLOCK, sourceId, side, MatBlockConditionFunc); } onAdd(_arena: Arena) { @@ -494,8 +565,9 @@ const CraftyShieldConditionFunc: ProtectConditionFunc = (_arena, moveId) => { * not target all Pokemon or sides of the field. */ class CraftyShieldTag extends ConditionalProtectTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.CRAFTY_SHIELD, MoveId.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true); + public readonly tagType = ArenaTagType.CRAFTY_SHIELD; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true); } } @@ -503,17 +575,8 @@ class CraftyShieldTag extends ConditionalProtectTag { * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Lucky_Chant_(move) Lucky Chant}. * Prevents critical hits against the tag's side. */ -export class NoCritTag extends ArenaTag { - /** - * 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); - } +export class NoCritTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.NO_CRIT; /** Queues a message upon adding this effect to the field */ onAdd(_arena: Arena): void { @@ -545,13 +608,17 @@ export class NoCritTag extends ArenaTag { * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) | Wish}. * Heals the Pokémon in the user's position the turn after Wish is used. */ -class WishTag extends ArenaTag { - private battlerIndex: BattlerIndex; - private triggerMessage: string; - private healHp: number; +class WishTag extends SerializableArenaTag { + // The following fields are meant to be inwardly mutable, but outwardly immutable. + readonly battlerIndex: BattlerIndex; + readonly healHp: number; + readonly sourceName: string; + // End inwardly mutable fields - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.WISH, turnCount, MoveId.WISH, sourceId, side); + public readonly tagType = ArenaTagType.WISH; + + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.WISH, sourceId, side); } onAdd(_arena: Arena): void { @@ -561,45 +628,38 @@ class WishTag extends ArenaTag { return; } - super.onAdd(_arena); - this.healHp = toDmgValue(source.getMaxHp() / 2); - - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:wishTagOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(source), - }), - ); + (this as Mutable).sourceName = getPokemonNameWithAffix(source); + (this as Mutable).healHp = toDmgValue(source.getMaxHp() / 2); + (this as Mutable).battlerIndex = source.getBattlerIndex(); } onRemove(_arena: Arena): void { const target = globalScene.getField()[this.battlerIndex]; if (target?.isActive(true)) { - globalScene.phaseManager.queueMessage(this.triggerMessage); + globalScene.phaseManager.queueMessage( + // TODO: Rename key as it triggers on activation + i18next.t("arenaTag:wishTagOnAdd", { + pokemonNameWithAffix: this.sourceName, + }), + ); globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), this.healHp, null, true, false); } } + + override loadTag(source: NonFunctionProperties): void { + super.loadTag(source); + (this as Mutable).battlerIndex = source.battlerIndex; + (this as Mutable).healHp = source.healHp; + (this as Mutable).sourceName = source.sourceName; + } } /** * Abstract class to implement weakened moves of a specific type. */ -export class WeakenMoveTypeTag extends ArenaTag { - private 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; - } +export abstract class WeakenMoveTypeTag extends SerializableArenaTag { + abstract readonly tagType: ArenaTagType.MUD_SPORT | ArenaTagType.WATER_SPORT; + abstract get weakenedType(): PokemonType; /** * Reduces an attack's power by 0.33x if it matches this tag's weakened type. @@ -623,8 +683,12 @@ export class WeakenMoveTypeTag extends ArenaTag { * Weakens Electric type moves for a set amount of turns, usually 5. */ class MudSportTag extends WeakenMoveTypeTag { - constructor(turnCount: number, sourceId: number) { - super(ArenaTagType.MUD_SPORT, turnCount, PokemonType.ELECTRIC, MoveId.MUD_SPORT, sourceId); + public readonly tagType = ArenaTagType.MUD_SPORT; + override get weakenedType(): PokemonType.ELECTRIC { + return PokemonType.ELECTRIC; + } + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.MUD_SPORT, sourceId); } onAdd(_arena: Arena): void { @@ -641,8 +705,12 @@ class MudSportTag extends WeakenMoveTypeTag { * Weakens Fire type moves for a set amount of turns, usually 5. */ class WaterSportTag extends WeakenMoveTypeTag { - constructor(turnCount: number, sourceId: number) { - super(ArenaTagType.WATER_SPORT, turnCount, PokemonType.FIRE, MoveId.WATER_SPORT, sourceId); + public readonly tagType = ArenaTagType.WATER_SPORT; + override get weakenedType(): PokemonType.FIRE { + return PokemonType.FIRE; + } + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.WATER_SPORT, sourceId); } onAdd(_arena: Arena): void { @@ -660,8 +728,9 @@ class WaterSportTag extends WeakenMoveTypeTag { * Converts Normal-type moves to Electric type for the rest of the turn. */ export class IonDelugeTag extends ArenaTag { + public readonly tagType = ArenaTagType.ION_DELUGE; constructor(sourceMove?: MoveId) { - super(ArenaTagType.ION_DELUGE, 1, sourceMove); + super(1, sourceMove); } /** Queues an on-add message */ @@ -690,7 +759,8 @@ export class IonDelugeTag extends ArenaTag { /** * 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 maxLayers: number; @@ -703,8 +773,8 @@ export class ArenaTrapTag extends ArenaTag { * @param side - The side (player or enemy) the tag affects. * @param maxLayers - The maximum amount of layers this tag can have. */ - constructor(tagType: ArenaTagType, sourceMove: MoveId, sourceId: number, side: ArenaTagSide, maxLayers: number) { - super(tagType, 0, sourceMove, sourceId, side); + constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide, maxLayers: number) { + super(0, sourceMove, sourceId, side); this.layers = 1; this.maxLayers = maxLayers; @@ -743,7 +813,7 @@ export class ArenaTrapTag extends ArenaTag { : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2); } - loadTag(source: any): void { + loadTag(source: NonFunctionProperties): void { super.loadTag(source); this.layers = source.layers; this.maxLayers = source.maxLayers; @@ -756,8 +826,9 @@ export class ArenaTrapTag extends ArenaTag { * in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap. */ class SpikesTag extends ArenaTrapTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.SPIKES, MoveId.SPIKES, sourceId, side, 3); + public readonly tagType = ArenaTagType.SPIKES; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.SPIKES, sourceId, side, 3); } onAdd(arena: Arena, quiet = false): void { @@ -814,11 +885,12 @@ class SpikesTag extends ArenaTrapTag { * Pokémon summoned into this trap remove it entirely. */ class ToxicSpikesTag extends ArenaTrapTag { - private neutralized: boolean; + #neutralized: boolean; + public readonly tagType = ArenaTagType.TOXIC_SPIKES; - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.TOXIC_SPIKES, MoveId.TOXIC_SPIKES, sourceId, side, 2); - this.neutralized = false; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.TOXIC_SPIKES, sourceId, side, 2); + this.#neutralized = false; } onAdd(arena: Arena, quiet = false): void { @@ -844,7 +916,7 @@ class ToxicSpikesTag extends ArenaTrapTag { } onRemove(arena: Arena): void { - if (!this.neutralized) { + if (!this.#neutralized) { super.onRemove(arena); } } @@ -855,7 +927,7 @@ class ToxicSpikesTag extends ArenaTrapTag { return true; } if (pokemon.isOfType(PokemonType.POISON)) { - this.neutralized = true; + this.#neutralized = true; if (globalScene.arena.removeTag(this.tagType)) { globalScene.phaseManager.queueMessage( i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { @@ -889,55 +961,15 @@ class ToxicSpikesTag 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 ArenaTag { - public targetIndex: BattlerIndex; - - constructor( - tagType: ArenaTagType, - sourceMove: MoveId | undefined, - sourceId: number, - targetIndex: BattlerIndex, - side: ArenaTagSide = ArenaTagSide.BOTH, - ) { - super(tagType, 3, sourceMove, sourceId, side); - - 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/Stealth_Rock_(move) Stealth Rock}. * Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon * who is summoned into the trap, based on the Rock type's type effectiveness. */ class StealthRockTag extends ArenaTrapTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.STEALTH_ROCK, MoveId.STEALTH_ROCK, sourceId, side, 1); + public readonly tagType = ArenaTagType.STEALTH_ROCK; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.STEALTH_ROCK, sourceId, side, 1); } onAdd(arena: Arena, quiet = false): void { @@ -1025,8 +1057,9 @@ class StealthRockTag extends ArenaTrapTag { * to any Pokémon who is summoned into this trap. */ class StickyWebTag extends ArenaTrapTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.STICKY_WEB, MoveId.STICKY_WEB, sourceId, side, 1); + public readonly tagType = ArenaTagType.STICKY_WEB; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.STICKY_WEB, sourceId, side, 1); } onAdd(arena: Arena, quiet = false): void { @@ -1093,14 +1126,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}. * 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. */ -export class TrickRoomTag extends ArenaTag { - constructor(turnCount: number, sourceId: number) { - super(ArenaTagType.TRICK_ROOM, turnCount, MoveId.TRICK_ROOM, sourceId); +export class TrickRoomTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.TRICK_ROOM; + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.TRICK_ROOM, sourceId); } /** @@ -1142,9 +1218,10 @@ export class TrickRoomTag extends ArenaTag { * 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. */ -export class GravityTag extends ArenaTag { - constructor(turnCount: number) { - super(ArenaTagType.GRAVITY, turnCount, MoveId.GRAVITY); +export class GravityTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.GRAVITY; + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.GRAVITY, sourceId); } onAdd(_arena: Arena): void { @@ -1170,9 +1247,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. * Applies this arena tag for 4 turns (including the turn the move was used). */ -class TailwindTag extends ArenaTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.TAILWIND, turnCount, MoveId.TAILWIND, sourceId, side); +class TailwindTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.TAILWIND; + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.TAILWIND, sourceId, side); } onAdd(_arena: Arena, quiet = false): void { @@ -1238,9 +1316,10 @@ class TailwindTag extends ArenaTag { * 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}. */ -class HappyHourTag extends ArenaTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.HAPPY_HOUR, turnCount, MoveId.HAPPY_HOUR, sourceId, side); +class HappyHourTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.HAPPY_HOUR; + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.HAPPY_HOUR, sourceId, side); } onAdd(_arena: Arena): void { @@ -1253,8 +1332,9 @@ class HappyHourTag extends ArenaTag { } class SafeguardTag extends ArenaTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.SAFEGUARD, turnCount, MoveId.SAFEGUARD, sourceId, side); + public readonly tagType = ArenaTagType.SAFEGUARD; + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.SAFEGUARD, sourceId, side); } onAdd(_arena: Arena): void { @@ -1275,18 +1355,21 @@ class SafeguardTag extends ArenaTag { } class NoneTag extends ArenaTag { + public readonly tagType = ArenaTagType.NONE; constructor() { - super(ArenaTagType.NONE, 0); + super(0); } } + /** * This arena tag facilitates the application of the move Imprison * Imprison remains in effect as long as the source Pokemon is active and present on the field. * Imprison will apply to any opposing Pokemon that switch onto the field as well. */ class ImprisonTag extends ArenaTrapTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.IMPRISON, MoveId.IMPRISON, sourceId, side, 1); + public readonly tagType = ArenaTagType.IMPRISON; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.IMPRISON, sourceId, side, 1); } /** @@ -1341,7 +1424,9 @@ class ImprisonTag extends ArenaTrapTag { */ override onRemove(): void { const party = this.getAffectedPokemon(); - party.forEach(p => p.removeTag(BattlerTagType.IMPRISON)); + party.forEach(p => { + p.removeTag(BattlerTagType.IMPRISON); + }); } } @@ -1352,9 +1437,10 @@ class ImprisonTag extends ArenaTrapTag { * Damages all non-Fire-type Pokemon on the given side of the field at the end * of each turn for 4 turns. */ -class FireGrassPledgeTag extends ArenaTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.FIRE_GRASS_PLEDGE, 4, MoveId.FIRE_PLEDGE, sourceId, side); +class FireGrassPledgeTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.FIRE_GRASS_PLEDGE; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(4, MoveId.FIRE_PLEDGE, sourceId, side); } override onAdd(_arena: Arena): void { @@ -1400,9 +1486,10 @@ class FireGrassPledgeTag extends ArenaTag { * Doubles the secondary effect chance of moves from Pokemon on the * given side of the field for 4 turns. */ -class WaterFirePledgeTag extends ArenaTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.WATER_FIRE_PLEDGE, 4, MoveId.WATER_PLEDGE, sourceId, side); +class WaterFirePledgeTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.WATER_FIRE_PLEDGE; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(4, MoveId.WATER_PLEDGE, sourceId, side); } override onAdd(_arena: Arena): void { @@ -1434,9 +1521,10 @@ class WaterFirePledgeTag extends ArenaTag { * 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. */ -class GrassWaterPledgeTag extends ArenaTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.GRASS_WATER_PLEDGE, 4, MoveId.GRASS_PLEDGE, sourceId, side); +class GrassWaterPledgeTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.GRASS_WATER_PLEDGE; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(4, MoveId.GRASS_PLEDGE, sourceId, side); } override onAdd(_arena: Arena): void { @@ -1456,9 +1544,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, * the Pokémon that replaces it will still be unable to switch out in the following turn. */ -export class FairyLockTag extends ArenaTag { - constructor(turnCount: number, sourceId: number) { - super(ArenaTagType.FAIRY_LOCK, turnCount, MoveId.FAIRY_LOCK, sourceId); +export class FairyLockTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.FAIRY_LOCK; + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.FAIRY_LOCK, sourceId); } onAdd(_arena: Arena): void { @@ -1472,15 +1561,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 * * Additionally ends onLose abilities when it is activated + * @sealed */ -export class SuppressAbilitiesTag extends ArenaTag { - private sourceCount: number; - private beingRemoved: boolean; +export class SuppressAbilitiesTag extends SerializableArenaTag { + // Source count is allowed to be inwardly mutable, but outwardly immutable + 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) { - super(ArenaTagType.NEUTRALIZING_GAS, 0, undefined, sourceId); + constructor(sourceId?: number) { + super(0, undefined, sourceId); this.sourceCount = 1; - this.beingRemoved = false; + this.#beingRemoved = false; + } + + public override loadTag(source: NonFunctionProperties): void { + super.loadTag(source); + (this as Mutable).sourceCount = source.sourceCount; } public override onAdd(_arena: Arena): void { @@ -1492,19 +1595,21 @@ export class SuppressAbilitiesTag extends ArenaTag { if (fieldPokemon && fieldPokemon.id !== pokemon.id) { // TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing // 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 { - this.sourceCount++; + (this as Mutable).sourceCount++; this.playActivationMessage(source); } public onSourceLeave(arena: Arena): void { - this.sourceCount--; + (this as Mutable).sourceCount--; if (this.sourceCount <= 0) { arena.removeTag(ArenaTagType.NEUTRALIZING_GAS); } else if (this.sourceCount === 1) { @@ -1522,7 +1627,7 @@ export class SuppressAbilitiesTag extends ArenaTag { } public override onRemove(_arena: Arena, quiet = false) { - this.beingRemoved = true; + this.#beingRemoved = true; if (!quiet) { globalScene.phaseManager.queueMessage(i18next.t("arenaTag:neutralizingGasOnRemove")); } @@ -1530,7 +1635,9 @@ export class SuppressAbilitiesTag extends ArenaTag { 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 if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) { - [true, false].forEach(passive => applyOnGainAbAttrs({ pokemon, passive })); + [true, false].forEach(passive => { + applyOnGainAbAttrs({ pokemon, passive }); + }); } } } @@ -1539,10 +1646,6 @@ export class SuppressAbilitiesTag extends ArenaTag { return this.sourceCount > 1; } - public isBeingRemoved() { - return this.beingRemoved; - } - private playActivationMessage(pokemon: Pokemon | null) { if (pokemon) { globalScene.phaseManager.queueMessage( @@ -1559,7 +1662,7 @@ export function getArenaTag( tagType: ArenaTagType, turnCount: number, sourceMove: MoveId | undefined, - sourceId: number, + sourceId: number | undefined, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH, ): ArenaTag | null { @@ -1575,7 +1678,7 @@ export function getArenaTag( case ArenaTagType.CRAFTY_SHIELD: return new CraftyShieldTag(sourceId, side); 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: return new MudSportTag(turnCount, sourceId); case ArenaTagType.WATER_SPORT: @@ -1588,7 +1691,10 @@ export function getArenaTag( return new ToxicSpikesTag(sourceId, side); case ArenaTagType.FUTURE_SIGHT: case ArenaTagType.DOOM_DESIRE: - return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex!, side); // TODO:questionable bang + if (!targetIndex) { + return null; // If missing target index, no tag is created + } + return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex, side); case ArenaTagType.WISH: return new WishTag(turnCount, sourceId, side); case ArenaTagType.STEALTH_ROCK: @@ -1598,7 +1704,7 @@ export function getArenaTag( case ArenaTagType.TRICK_ROOM: return new TrickRoomTag(turnCount, sourceId); case ArenaTagType.GRAVITY: - return new GravityTag(turnCount); + return new GravityTag(turnCount, sourceId); case ArenaTagType.REFLECT: return new ReflectTag(turnCount, sourceId, side); case ArenaTagType.LIGHT_SCREEN: @@ -1630,10 +1736,10 @@ export function getArenaTag( /** * 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 - * @return {ArenaTag} The valid arena tag + * @param source - An arena tag + * @returns The valid arena tag */ -export function loadArenaTag(source: ArenaTag | any): ArenaTag { +export function loadArenaTag(source: (ArenaTag | ArenaTagTypeData) & { targetIndex?: BattlerIndex }): ArenaTag { const tag = getArenaTag( source.tagType, @@ -1646,3 +1752,37 @@ export function loadArenaTag(source: ArenaTag | any): ArenaTag { tag.loadTag(source); 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.FUTURE_SIGHT]: DelayedAttackTag; + [ArenaTagType.DOOM_DESIRE]: DelayedAttackTag; + [ArenaTagType.WISH]: WishTag; + [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; +}; diff --git a/src/data/terrain.ts b/src/data/terrain.ts index f44fd4f35e7..f5382b1c3ec 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -13,6 +13,11 @@ export enum TerrainType { PSYCHIC, } +export interface SerializedTerrain { + terrainType: TerrainType; + turnsLeft: number; +} + export class Terrain { public terrainType: TerrainType; public turnsLeft: number; diff --git a/src/data/weather.ts b/src/data/weather.ts index 62a03aa0832..59be56826a4 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -11,6 +11,11 @@ import type { Move } from "#moves/move"; import { randSeedInt } from "#utils/common"; import i18next from "i18next"; +export interface SerializedWeather { + weatherType: WeatherType; + turnsLeft: number; +} + export class Weather { public weatherType: WeatherType; public turnsLeft: number; diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 4180aa00ef5..214826993b3 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -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 { NONE = "NONE", MUD_SPORT = "MUD_SPORT", diff --git a/src/field/arena.ts b/src/field/arena.ts index b28ebd0b46b..8f27ddb22e9 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -30,6 +30,7 @@ import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEven import type { Pokemon } from "#field/pokemon"; import { FieldEffectModifier } from "#modifiers/modifier"; import type { Move } from "#moves/move"; +import type { AbstractConstructor } from "#types/type-helpers"; import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; @@ -644,7 +645,7 @@ export class Arena { * @param args array of parameters that the called upon tags may need */ applyTagsForSide( - tagType: ArenaTagType | Constructor, + tagType: ArenaTagType | Constructor | AbstractConstructor, side: ArenaTagSide, simulated: boolean, ...args: unknown[] @@ -666,7 +667,11 @@ export class Arena { * @param simulated if `true`, this applies arena tags without changing game state * @param args array of parameters that the called upon tags may need */ - applyTags(tagType: ArenaTagType | Constructor, simulated: boolean, ...args: unknown[]): void { + applyTags( + tagType: ArenaTagType | Constructor | AbstractConstructor, + simulated: boolean, + ...args: unknown[] + ): void { this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args); } @@ -723,7 +728,7 @@ export class Arena { * @param tagType The {@linkcode ArenaTagType} or {@linkcode ArenaTag} to get * @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there */ - getTag(tagType: ArenaTagType | Constructor): ArenaTag | undefined { + getTag(tagType: ArenaTagType | Constructor | AbstractConstructor): ArenaTag | undefined { return this.getTagOnSide(tagType, ArenaTagSide.BOTH); } @@ -739,7 +744,10 @@ export class Arena { * @param side The {@linkcode ArenaTagSide} to look at * @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there */ - getTagOnSide(tagType: ArenaTagType | Constructor, side: ArenaTagSide): ArenaTag | undefined { + getTagOnSide( + tagType: ArenaTagType | Constructor | AbstractConstructor, + side: ArenaTagSide, + ): ArenaTag | undefined { return typeof tagType === "string" ? this.tags.find( t => t.tagType === tagType && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 34d8c2b365a..01f137b28fd 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2192,7 +2192,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } const suppressAbilitiesTag = arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag; 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 hasSuppressingAbility = this.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false); // Neutralizing gas is up - suppress abilities unless they are unsuppressable or this pokemon is responsible for the gas diff --git a/src/system/arena-data.ts b/src/system/arena-data.ts index 9d15ab50fcc..c0ad4a25024 100644 --- a/src/system/arena-data.ts +++ b/src/system/arena-data.ts @@ -1,9 +1,19 @@ import type { ArenaTag } from "#data/arena-tag"; -import { loadArenaTag } from "#data/arena-tag"; +import { loadArenaTag, SerializableArenaTag } from "#data/arena-tag"; import { Terrain } from "#data/terrain"; import { Weather } from "#data/weather"; import type { BiomeId } from "#enums/biome-id"; 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 | null; + terrain: NonFunctionProperties | null; + tags?: ArenaTagTypeData[]; + playerTerasUsed?: number; +} export class ArenaData { public biome: BiomeId; @@ -12,24 +22,26 @@ export class ArenaData { public tags: ArenaTag[]; public playerTerasUsed: number; - constructor(source: Arena | any) { - const sourceArena = source instanceof Arena ? (source as Arena) : null; - this.biome = sourceArena ? sourceArena.biomeType : source.biome; - this.weather = sourceArena - ? sourceArena.weather - : source.weather - ? new Weather(source.weather.weatherType, source.weather.turnsLeft) - : null; - 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 = []; + constructor(source: Arena | SerializedArenaData) { + // Exclude any unserializable tags from the serialized data (such as ones only lasting 1 turn). + // NOTE: The filter has to be done _after_ map, data loaded from `ArenaTagTypeData` + // is not yet an instance of `ArenaTag` + this.tags = + source.tags + ?.map((t: ArenaTag | ArenaTagTypeData) => loadArenaTag(t)) + ?.filter((tag): tag is SerializableArenaTag => tag instanceof SerializableArenaTag) ?? []; - if (source.tags) { - this.tags = source.tags.map(t => loadArenaTag(t)); + this.playerTerasUsed = source.playerTerasUsed ?? 0; + + if (source instanceof Arena) { + this.biome = source.biomeType; + this.weather = source.weather; + this.terrain = source.terrain; + return; } + + 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; } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 62603260cb2..4f251b212b1 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -42,7 +42,7 @@ import * as Modifier from "#modifiers/modifier"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import type { Variant } from "#sprites/variant"; 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 { EggData } from "#system/egg-data"; import { GameStats } from "#system/game-stats"; @@ -1286,7 +1286,7 @@ export class GameData { } case "arena": - return new ArenaData(v); + return new ArenaData(v as SerializedArenaData); case "challenges": { const ret: ChallengeData[] = [];