diff --git a/package.json b/package.json index 27b1fc4e290..1fb25c5d4ba 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "lefthook": "^1.12.2", "msw": "^2.10.4", "phaser3spectorjs": "^0.0.8", - "typedoc": "0.28.7", + "typedoc": "^0.28.13", "typedoc-github-theme": "^0.3.1", "typedoc-plugin-coverage": "^4.0.1", "typedoc-plugin-mdn-links": "^5.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46608772338..50a8b17b366 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,17 +88,17 @@ importers: specifier: ^0.0.8 version: 0.0.8 typedoc: - specifier: 0.28.7 - version: 0.28.7(typescript@5.9.2) + specifier: ^0.28.13 + version: 0.28.13(typescript@5.9.2) typedoc-github-theme: specifier: ^0.3.1 - version: 0.3.1(typedoc@0.28.7(typescript@5.9.2)) + version: 0.3.1(typedoc@0.28.13(typescript@5.9.2)) typedoc-plugin-coverage: specifier: ^4.0.1 - version: 4.0.1(typedoc@0.28.7(typescript@5.9.2)) + version: 4.0.1(typedoc@0.28.13(typescript@5.9.2)) typedoc-plugin-mdn-links: specifier: ^5.0.9 - version: 5.0.9(typedoc@0.28.7(typescript@5.9.2)) + version: 5.0.9(typedoc@0.28.13(typescript@5.9.2)) typescript: specifier: ^5.9.2 version: 5.9.2 @@ -717,17 +717,17 @@ packages: cpu: [x64] os: [win32] - '@shikijs/engine-oniguruma@3.12.2': - resolution: {integrity: sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==} + '@shikijs/engine-oniguruma@3.13.0': + resolution: {integrity: sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==} - '@shikijs/langs@3.12.2': - resolution: {integrity: sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==} + '@shikijs/langs@3.13.0': + resolution: {integrity: sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==} - '@shikijs/themes@3.12.2': - resolution: {integrity: sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==} + '@shikijs/themes@3.13.0': + resolution: {integrity: sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==} - '@shikijs/types@3.12.2': - resolution: {integrity: sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==} + '@shikijs/types@3.13.0': + resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1828,12 +1828,12 @@ packages: peerDependencies: typedoc: 0.27.x || 0.28.x - typedoc@0.28.7: - resolution: {integrity: sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==} + typedoc@0.28.13: + resolution: {integrity: sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} @@ -2312,10 +2312,10 @@ snapshots: '@gerrit0/mini-shiki@3.12.2': dependencies: - '@shikijs/engine-oniguruma': 3.12.2 - '@shikijs/langs': 3.12.2 - '@shikijs/themes': 3.12.2 - '@shikijs/types': 3.12.2 + '@shikijs/engine-oniguruma': 3.13.0 + '@shikijs/langs': 3.13.0 + '@shikijs/themes': 3.13.0 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 '@inquirer/checkbox@4.2.0(@types/node@22.16.5)': @@ -2547,20 +2547,20 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true - '@shikijs/engine-oniguruma@3.12.2': + '@shikijs/engine-oniguruma@3.13.0': dependencies: - '@shikijs/types': 3.12.2 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.12.2': + '@shikijs/langs@3.13.0': dependencies: - '@shikijs/types': 3.12.2 + '@shikijs/types': 3.13.0 - '@shikijs/themes@3.12.2': + '@shikijs/themes@3.13.0': dependencies: - '@shikijs/types': 3.12.2 + '@shikijs/types': 3.13.0 - '@shikijs/types@3.12.2': + '@shikijs/types@3.13.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -3682,19 +3682,19 @@ snapshots: type-fest@4.41.0: {} - typedoc-github-theme@0.3.1(typedoc@0.28.7(typescript@5.9.2)): + typedoc-github-theme@0.3.1(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.9.2) + typedoc: 0.28.13(typescript@5.9.2) - typedoc-plugin-coverage@4.0.1(typedoc@0.28.7(typescript@5.9.2)): + typedoc-plugin-coverage@4.0.1(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.9.2) + typedoc: 0.28.13(typescript@5.9.2) - typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.7(typescript@5.9.2)): + typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.9.2) + typedoc: 0.28.13(typescript@5.9.2) - typedoc@0.28.7(typescript@5.9.2): + typedoc@0.28.13(typescript@5.9.2): dependencies: '@gerrit0/mini-shiki': 3.12.2 lunr: 2.3.9 diff --git a/src/@types/battler-tags.ts b/src/@types/battler-tags.ts index e47b4f8cfc1..ec72c811447 100644 --- a/src/@types/battler-tags.ts +++ b/src/@types/battler-tags.ts @@ -89,7 +89,8 @@ export type AbilityBattlerTagType = | BattlerTagType.QUARK_DRIVE | BattlerTagType.UNBURDEN | BattlerTagType.SLOW_START - | BattlerTagType.TRUANT; + | BattlerTagType.TRUANT + | BattlerTagType.SUPREME_OVERLORD; /** Subset of {@linkcode BattlerTagType}s that provide type boosts */ export type TypeBoostTagType = BattlerTagType.FIRE_BOOST | BattlerTagType.CHARGED; diff --git a/src/@types/damage-params.ts b/src/@types/damage-params.ts new file mode 100644 index 00000000000..b656c60f0ab --- /dev/null +++ b/src/@types/damage-params.ts @@ -0,0 +1,44 @@ +import type { MoveCategory } from "#enums/move-category"; +import type { Pokemon } from "#field/pokemon"; +import type { Move } from "#types/move-types"; + +/** + * Collection of types for methods like {@linkcode Pokemon#getBaseDamage} and {@linkcode Pokemon#getAttackDamage}. + * @module + */ + +/** Base type for damage parameter methods, used for DRY */ +export interface damageParams { + /** The attacking {@linkcode Pokemon} */ + source: Pokemon; + /** The move used in the attack */ + move: Move; + /** The move's {@linkcode MoveCategory} after variable-category effects are applied */ + moveCategory: MoveCategory; + /** If `true`, ignores this Pokemon's defensive ability effects */ + ignoreAbility?: boolean; + /** If `true`, ignores the attacking Pokemon's ability effects */ + ignoreSourceAbility?: boolean; + /** If `true`, ignores the ally Pokemon's ability effects */ + ignoreAllyAbility?: boolean; + /** If `true`, ignores the ability effects of the attacking pokemon's ally */ + ignoreSourceAllyAbility?: boolean; + /** If `true`, calculates damage for a critical hit */ + isCritical?: boolean; + /** If `true`, suppresses changes to game state during the calculation */ + simulated?: boolean; + /** If defined, used in place of calculated effectiveness values */ + effectiveness?: number; +} + +/** + * Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} + * @interface + */ +export type getBaseDamageParams = Omit; + +/** + * Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} + * @interface + */ +export type getAttackDamageParams = Omit; diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 790383b3bf5..f48511ba375 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -6961,7 +6961,7 @@ export function initAbilities() { .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPD, 1) .ignorable(), new Ability(AbilityId.RIVALRY, 4) - .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25, true) + .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25) .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75), new Ability(AbilityId.STEADFAST, 4) .attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1), @@ -7751,8 +7751,8 @@ export function initAbilities() { new Ability(AbilityId.SHARPNESS, 9) .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(AbilityId.SUPREME_OVERLORD, 9) - .attr(VariableMovePowerBoostAbAttr, (user, _target, _move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5)) - .partial(), // Should only boost once, on summon + .conditionalAttr((p) => (p.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints) > 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.SUPREME_OVERLORD, 0, true) + .edgeCase(), // Tag is not tied to ability, so suppression/removal etc will not function until a structure to allow this is implemented new Ability(AbilityId.COSTAR, 9, -2) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(AbilityId.TOXIC_DEBRIS, 9) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 7d78076e06b..fd64e271758 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -56,6 +56,7 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { HitResult } from "#enums/hit-result"; import { CommonAnim } from "#enums/move-anims-common"; @@ -1597,6 +1598,145 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { } } +/** + * Interface containing data related to a queued healing effect from + * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish} + * or {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}. + */ +interface PendingHealEffect { + /** The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} that created the effect. */ + readonly sourceId: number; + /** The {@linkcode MoveId} of the move that created the effect. */ + readonly moveId: MoveId; + /** If `true`, also restores the target's PP when the effect activates. */ + readonly restorePP: boolean; + /** The message to display when the effect activates */ + readonly healMessage: string; +} + +/** + * Arena tag to contain stored healing effects, namely from + * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}. + * When a damaged Pokemon first enters the effect's {@linkcode BattlerIndex | field position}, + * their HP is fully restored, and they are cured of any non-volatile status condition. + * If the effect is from Lunar Dance, their PP is also restored. + */ +export class PendingHealTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.PENDING_HEAL; + /** All pending healing effects, organized by {@linkcode BattlerIndex} */ + public readonly pendingHeals: Partial> = {}; + + constructor() { + super(0); + } + + /** + * Adds a pending healing effect to the field. Effects under the same move *and* + * target index as an existing effect are ignored. + * @param targetIndex - The {@linkcode BattlerIndex} under which the effect applies + * @param healEffect - The {@linkcode PendingHealEffect | data} for the pending heal effect + */ + public queueHeal(targetIndex: BattlerIndex, healEffect: PendingHealEffect): void { + const existingHealEffects = this.pendingHeals[targetIndex]; + if (existingHealEffects) { + if (!existingHealEffects.some(he => he.moveId === healEffect.moveId)) { + existingHealEffects.push(healEffect); + } + } else { + this.pendingHeals[targetIndex] = [healEffect]; + } + } + + /** Removes default on-remove message */ + override onRemove(_arena: Arena): void {} + + /** This arena tag is removed at the end of the turn if no pending healing effects are on the field */ + override lapse(_arena: Arena): boolean { + for (const key in this.pendingHeals) { + if (this.pendingHeals[key].length > 0) { + return true; + } + } + return false; + } + + /** + * Applies a pending healing effect on the given target index. If an effect is found for + * the index, the Pokemon at that index is healed to full HP, is cured of any non-volatile status, + * and has its PP fully restored (if the effect is from Lunar Dance). + * @param arena - The {@linkcode Arena} containing this tag + * @param simulated - If `true`, suppresses changes to game state + * @param pokemon - The {@linkcode Pokemon} receiving the healing effect + * @returns `true` if the target Pokemon was healed by this effect + * @todo This should also be called when a Pokemon moves into a new position via Ally Switch + */ + override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean { + const targetIndex = pokemon.getBattlerIndex(); + const targetEffects = this.pendingHeals[targetIndex]; + + if (targetEffects == null || targetEffects.length === 0) { + return false; + } + + const healEffect = targetEffects.find(effect => this.canApply(effect, pokemon)); + + if (healEffect == null) { + return false; + } + + if (simulated) { + return true; + } + + const { sourceId, moveId, restorePP, healMessage } = healEffect; + const sourcePokemon = globalScene.getPokemonById(sourceId); + if (!sourcePokemon) { + console.warn(`Source of pending ${allMoves[moveId].name} effect is undefined!`); + targetEffects.splice(targetEffects.indexOf(healEffect), 1); + // Re-evaluate after the invalid heal effect is removed + return this.apply(arena, simulated, pokemon); + } + + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + targetIndex, + pokemon.getMaxHp(), + healMessage, + true, + false, + false, + true, + false, + restorePP, + ); + + targetEffects.splice(targetEffects.indexOf(healEffect), 1); + + return healEffect != null; + } + + /** + * Determines if the given {@linkcode PendingHealEffect} can immediately heal + * the given target {@linkcode Pokemon}. + * @param healEffect - The {@linkcode PendingHealEffect} to evaluate + * @param pokemon - The {@linkcode Pokemon} to evaluate against + * @returns `true` if the Pokemon can be healed by the effect + */ + private canApply(healEffect: PendingHealEffect, pokemon: Pokemon): boolean { + return ( + !pokemon.isFullHp() + || pokemon.status != null + || (healEffect.restorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0)) + ); + } + + override loadTag(source: BaseArenaTag & Pick): void { + super.loadTag(source); + (this as Mutable).pendingHeals = source.pendingHeals; + } +} + // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter export function getArenaTag( tagType: ArenaTagType, @@ -1660,6 +1800,8 @@ export function getArenaTag( return new FairyLockTag(turnCount, sourceId); case ArenaTagType.NEUTRALIZING_GAS: return new SuppressAbilitiesTag(sourceId); + case ArenaTagType.PENDING_HEAL: + return new PendingHealTag(); default: return null; } @@ -1708,5 +1850,6 @@ export type ArenaTagTypeMap = { [ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag; [ArenaTagType.FAIRY_LOCK]: FairyLockTag; [ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag; + [ArenaTagType.PENDING_HEAL]: PendingHealTag; [ArenaTagType.NONE]: NoneTag; }; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index cf46e22c1dd..9ae338a7cf7 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -3674,6 +3674,41 @@ export class MagicCoatTag extends BattlerTag { } } +/** + * Tag associated with {@linkcode AbilityId.SUPREME_OVERLORD} + */ +export class SupremeOverlordTag extends AbilityBattlerTag { + public override readonly tagType = BattlerTagType.SUPREME_OVERLORD; + /** The number of faints at the time the user was sent out */ + public readonly faintCount: number; + constructor() { + super(BattlerTagType.SUPREME_OVERLORD, AbilityId.SUPREME_OVERLORD, BattlerTagLapseType.FAINT, 0); + } + + public override onAdd(pokemon: Pokemon): boolean { + (this as Mutable).faintCount = Math.min( + pokemon.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, + 5, + ); + globalScene.phaseManager.queueMessage( + i18next.t("battlerTags:supremeOverlordOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), + ); + return true; + } + + /** + * @returns The damage multiplier for Supreme Overlord + */ + public getBoost(): number { + return 1 + 0.1 * this.faintCount; + } + + public override loadTag(source: BaseBattlerTag & Pick): void { + super.loadTag(source); + (this as Mutable).faintCount = source.faintCount; + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @param sourceId - The ID of the pokemon adding the tag @@ -3874,6 +3909,8 @@ export function getBattlerTag( return new PsychoShiftTag(); case BattlerTagType.MAGIC_COAT: return new MagicCoatTag(); + case BattlerTagType.SUPREME_OVERLORD: + return new SupremeOverlordTag(); } } @@ -4008,4 +4045,5 @@ export type BattlerTagTypeMap = { [BattlerTagType.GRUDGE]: GrudgeTag; [BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag; [BattlerTagType.MAGIC_COAT]: MagicCoatTag; + [BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag; }; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index bf4abacd895..4ab1cc94702 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account"; import type { GameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { EntryHazardTag } from "#data/arena-tag"; +import type { EntryHazardTag, PendingHealTag } from "#data/arena-tag"; import { WeakenMoveTypeTag } from "#data/arena-tag"; import { MoveChargeAnim } from "#data/battle-anims"; import { @@ -19,6 +19,7 @@ import { ShellTrapTag, StockpilingTag, SubstituteTag, + SupremeOverlordTag, TrappedTag, TypeBoostTag, } from "#data/battler-tags"; @@ -869,6 +870,8 @@ export abstract class Move implements Localizable { power.value *= 1.5; } + power.value *= (source.getTag(BattlerTagType.SUPREME_OVERLORD) as SupremeOverlordTag | undefined)?.getBoost() ?? 1; + return power.value; } @@ -2137,24 +2140,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { return false; } - // We don't know which party member will be chosen, so pick the highest max HP in the party - const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); - const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0); - - const pm = globalScene.phaseManager; - - pm.pushPhase( - pm.create("PokemonHealPhase", - user.getBattlerIndex(), - maxPartyMemberHp, - i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), - true, - false, - false, - true, - false, - this.restorePP), - true); + // Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot. + globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused + const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag; + tag.queueHeal(user.getBattlerIndex(), { + sourceId: user.id, + moveId: move.id, + restorePP: this.restorePP, + healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), + }); return true; } diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 30f053b98bd..717845cf2d9 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -34,4 +34,5 @@ export enum ArenaTagType { GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE", FAIRY_LOCK = "FAIRY_LOCK", NEUTRALIZING_GAS = "NEUTRALIZING_GAS", + PENDING_HEAL = "PENDING_HEAL", } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 6d9d2dd4a92..7956e506886 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -94,4 +94,5 @@ export enum BattlerTagType { ENDURE_TOKEN = "ENDURE_TOKEN", POWDER = "POWDER", MAGIC_COAT = "MAGIC_COAT", + SUPREME_OVERLORD = "SUPREME_OVERLORD", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d8b21704ed9..01ed747ad38 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -141,6 +141,7 @@ import type { PokemonData } from "#system/pokemon-data"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types"; +import type { getAttackDamageParams, getBaseDamageParams } from "#types/damage-params"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; import type { IllusionData } from "#types/illusion-data"; import type { StarterDataEntry, StarterMoveset } from "#types/save-data"; @@ -176,36 +177,6 @@ import i18next from "i18next"; import Phaser from "phaser"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -/** Base typeclass for damage parameter methods, used for DRY */ -type damageParams = { - /** The attacking {@linkcode Pokemon} */ - source: Pokemon; - /** The move used in the attack */ - move: Move; - /** The move's {@linkcode MoveCategory} after variable-category effects are applied */ - moveCategory: MoveCategory; - /** If `true`, ignores this Pokemon's defensive ability effects */ - ignoreAbility?: boolean; - /** If `true`, ignores the attacking Pokemon's ability effects */ - ignoreSourceAbility?: boolean; - /** If `true`, ignores the ally Pokemon's ability effects */ - ignoreAllyAbility?: boolean; - /** If `true`, ignores the ability effects of the attacking pokemon's ally */ - ignoreSourceAllyAbility?: boolean; - /** If `true`, calculates damage for a critical hit */ - isCritical?: boolean; - /** If `true`, suppresses changes to game state during the calculation */ - simulated?: boolean; - /** If defined, used in place of calculated effectiveness values */ - effectiveness?: number; -}; - -/** Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} */ -type getBaseDamageParams = Omit; - -/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */ -type getAttackDamageParams = Omit; - export abstract class Pokemon extends Phaser.GameObjects.Container { /** * This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID}, @@ -242,20 +213,46 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @todo Make private */ public status: Status | null; + /** + * The Pokémon's current friendship value, ranging from 0 to 255. + * @see {@link https://bulbapedia.bulbagarden.net/wiki/Friendship} + */ public friendship: number; + /** + * The level at which this Pokémon was met + * @remarks + * Primarily used for displaying in the summary screen + */ public metLevel: number; + /** + * The ID of the biome this Pokémon was met in + * @remarks + * Primarily used for display in the summary screen. + */ public metBiome: BiomeId | -1; + // TODO: figure out why this is used and document it (seems only to be read for getting the Pokémon's egg moves) public metSpecies: SpeciesId; + /** The wave index at which this Pokémon was met/encountered */ public metWave: number; public luck: number; public pauseEvolutions: boolean; public pokerus: boolean; + /** + * Indicates whether this Pokémon has left or is about to leave the field + * @remarks + * When `true` on a Wild Pokemon, this indicates it is about to flee. + */ public switchOutStatus = false; public evoCounter: number; + /** The type this Pokémon turns into when Terastallized */ public teraType: PokemonType; + /** Whether this Pokémon is currently Terastallized */ public isTerastallized: boolean; + /** The set of Types that have been boosted by this Pokémon's Stellar Terastallization. */ public stellarTypesBoosted: PokemonType[]; + // TODO: Create a fusionData class / interface and move all fusion-related fields there, exposed via getters + /** If this Pokémon is a fusion, the species it is fused with; `null` if not a fusion */ public fusionSpecies: PokemonSpecies | null; public fusionFormIndex: number; public fusionAbilityIndex: number; @@ -287,11 +284,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; + /** The position of this Pokémon on the field */ public fieldPosition: FieldPosition; public maskEnabled: boolean; public maskSprite: Phaser.GameObjects.Sprite | null; + /** + * The set of all TMs that have been used on this Pokémon + * + * @remarks + * Used to allow re-learning TM moves via, e.g., the Memory Mushroom + */ public usedTMs: MoveId[]; private shinySparkle: Phaser.GameObjects.Sprite; @@ -516,7 +520,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { abstract initBattleInfo(): void; - isOnField(): boolean { + public isOnField(): boolean { if (!globalScene) { return false; } @@ -568,7 +572,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.isAllowedInBattle() && (!onField || this.isOnField()); } - getDexAttr(): bigint { + public getDexAttr(): bigint { let ret = 0n; if (this.gender !== Gender.GENDERLESS) { ret |= this.gender !== Gender.FEMALE ? DexAttr.MALE : DexAttr.FEMALE; @@ -582,9 +586,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Sets the Pokemon's name. Only called when loading a Pokemon so this function needs to be called when * initializing hardcoded Pokemon or else it will not display the form index name properly. - * @returns n/a */ - generateName(): void { + public generateName(): void { if (!this.fusionSpecies) { this.name = this.species.getName(this.formIndex); return; @@ -838,11 +841,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on * non-experimental sprites before giving up. * - * @param cacheKey the cache key for the variant color sprite - * @param attemptedSpritePath the sprite path that failed to load - * @param useExpSprite was the attempted sprite experimental - * @param battleSpritePath the filename of the sprite - * @param optionalParams any additional params to log + * @param cacheKey - The cache key for the variant color sprite + * @param attemptedSpritePath - The sprite path that failed to load + * @param useExpSprite - Whether the attempted sprite was experimental + * @param battleSpritePath - The filename of the sprite + * @param optionalParams - Any additional params to log */ async fallbackVariantColor( cacheKey: string, @@ -908,6 +911,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.fusionSpecies.forms[this.fusionFormIndex].formKey; } + //#region Atlas and sprite ID methods // 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 @@ -1036,6 +1040,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionVariant, ); } + //#endregion Atlas and sprite ID methods /** * Return this Pokemon's {@linkcode PokemonSpeciesForm | SpeciesForm}. @@ -1065,7 +1070,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * (such as by the effects of {@linkcode MoveId.TRANSFORM} or {@linkcode AbilityId.IMPOSTER}. * @returns Whether this Pokemon is currently transformed. */ - isTransformed(): boolean { + public isTransformed(): boolean { return this.summonData.speciesForm !== null; } @@ -1074,7 +1079,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param target - The {@linkcode Pokemon} being transformed into * @returns Whether this Pokemon can transform into `target`. */ - canTransformInto(target: Pokemon): boolean { + public canTransformInto(target: Pokemon): boolean { return !( // Neither pokemon can be already transformed ( @@ -1095,7 +1100,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @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 = false, useIllusion = false): PokemonSpeciesForm { + public getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm { const fusionSpecies: PokemonSpecies = useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!; const fusionFormIndex = @@ -1140,7 +1145,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** Resets the pokemon's field sprite properties, including position, alpha, and scale */ - resetSprite(): void { + public resetSprite(): void { // Resetting properties should not be shown on the field this.setVisible(false); @@ -1191,9 +1196,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Attempts to animate a given {@linkcode Phaser.GameObjects.Sprite} * @see {@linkcode Phaser.GameObjects.Sprite.play} - * @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate - * @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint - * @param animConfig {@linkcode String} to pass to {@linkcode Phaser.GameObjects.Sprite.play} + * @param sprite - Sprite to animate + * @param tintSprite - Sprite placed on top of the sprite to add a color tint + * @param animConfig - String to pass to the sprite's {@linkcode Phaser.GameObjects.Sprite.play | play} method * @returns true if the sprite was able to be animated */ tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, key: string): boolean { @@ -1258,7 +1263,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - setFieldPosition(fieldPosition: FieldPosition, duration?: number): Promise { + /** + * Set the field position of this Pokémon + * @param fieldPosition - The new field position + * @param duration - How long the transition should take, in milliseconds; if `0` or `undefined`, the position is changed instantly + */ + public setFieldPosition(fieldPosition: FieldPosition, duration?: number): Promise { return new Promise(resolve => { if (fieldPosition === this.fieldPosition) { resolve(); @@ -1608,10 +1618,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return baseStats; } + // TODO: Convert this into a getter getNature(): Nature { return this.customPokemonData.nature !== -1 ? this.customPokemonData.nature : this.nature; } + // TODO: Convert this into a setter OR just add a listener for calculateStats... setNature(nature: Nature): void { this.nature = nature; this.calculateStats(); @@ -1622,7 +1634,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - generateNature(naturePool?: Nature[]): void { + /** + * Randomly generate and set this Pokémon's nature + * @param naturePool - An optional array of Natures to choose from. If not provided, all natures will be considered. + */ + private generateNature(naturePool?: Nature[]): void { if (naturePool === undefined) { naturePool = getEnumValues(Nature); } @@ -1630,10 +1646,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.setNature(nature); } + // TODO: Convert this into a getter isFullHp(): boolean { return this.hp >= this.getMaxHp(); } + // TODO: Convert this into a getter getMaxHp(): number { return this.getStat(Stat.HP); } @@ -1643,6 +1661,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.getMaxHp() - this.hp; } + /** + * Return the ratio of this Pokémon's current HP to its maximum HP + * @param precise - Whether to return the exact HP ratio (e.g. `0.54321`), or one rounded to the nearest %; default `false` + * @returns The current HP ratio + */ getHpRatio(precise = false): number { return precise ? this.hp / this.getMaxHp() : Math.round((this.hp / this.getMaxHp()) * 100) / 100; } @@ -1680,18 +1703,30 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Check whether this Pokemon is shiny. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * Check whether this Pokémon is shiny, including its fusion species + * + * @param useIllusion - Whether to consider an active illusion * @returns Whether this Pokemon is shiny + * @see {@linkcode isBaseShiny} */ isShiny(useIllusion = false): boolean { return this.isBaseShiny(useIllusion) || this.isFusionShiny(useIllusion); } + /** + * Get whether this Pokémon's _base_ species is shiny + * @param useIllusion - Whether to consider an active illusion; default `false` + * @returns Whether the pokemon is shiny + */ isBaseShiny(useIllusion = false) { return useIllusion ? (this.summonData.illusion?.shiny ?? this.shiny) : this.shiny; } + /** + * Get whether this Pokémon's _fusion_ species is shiny + * @param useIllusion - Whether to consider an active illusion; default `true` + * @returns Whether this Pokémon's fusion species is shiny, or `false` if there is no fusion + */ isFusionShiny(useIllusion = false) { if (!this.isFusion(useIllusion)) { return false; @@ -1701,7 +1736,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * 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` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns Whether this pokemon's base and fusion counterparts are both shiny. */ isDoubleShiny(useIllusion = false): boolean { @@ -1709,10 +1744,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Return this Pokemon's {@linkcode Variant | shiny variant}. + * Return this Pokemon's shiny variant. * If a fusion, returns the maximum of the two variants. * Only meaningful if this pokemon is actually shiny. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns The shiny variant of this Pokemon. */ getVariant(useIllusion = false): Variant { @@ -1727,6 +1762,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return the base pokemon's variant. Equivalent to {@linkcode getVariant} if this pokemon is not a fusion. + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns The shiny variant of this Pokemon's base species. */ getBaseVariant(useIllusion = false): Variant { @@ -1735,10 +1771,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Return the fused pokemon's variant. + * Get the shiny variant of this Pokémon's _fusion_ species * * @remarks * Always returns `0` if the pokemon is not a fusion. + * @param useIllusion - Whether to consider an active illusion * @returns The shiny variant of this pokemon's fusion species. */ getFusionVariant(useIllusion = false): Variant { @@ -1759,7 +1796,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return whether this {@linkcode Pokemon} is currently fused with anything. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns Whether this Pokemon is currently fused with another species. */ isFusion(useIllusion = false): boolean { @@ -1768,7 +1805,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return this {@linkcode Pokemon}'s name. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns This Pokemon's name. * @see {@linkcode getNameToRender} - gets this Pokemon's display name. */ @@ -1874,8 +1911,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @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 MoveId.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). + * @param useIllusion - Whether to consider an active illusion; default `false` + * @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or perceived). */ public getTypes( includeTeraType = false, @@ -2083,10 +2120,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon + * Set this Pokémon's temporary ability, activating it if it normally activates on summon * * Also clears primal weather if it is from the ability being changed - * @param ability New Ability + * @param ability - The temporary ability to set + * @param passive - Whether to set the passive ability instead of the non-passive one; default `false` */ public setTempAbility(ability: Ability, passive = false): void { applyOnLoseAbAttrs({ pokemon: this, passive }); @@ -2146,11 +2184,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks whether an ability of a pokemon can be currently applied. This should rarely be + * Check whether this Pokémon can apply its current ability + * + * @remarks + * This should rarely be * directly called, as {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} already call this. - * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param passive If true, check if passive can be applied instead of non-passive - * @returns `true` if the ability can be applied + * @param passive - Whether to check the passive (`true`) or non-passive (`false`) ability; default `false` + * @returns Whether the ability can be applied */ public canApplyAbility(passive = false): boolean { if (passive && !this.hasPassive()) { @@ -2365,14 +2405,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the effectiveness of a move against the Pokémon. - * This includes modifiers from move and ability attributes. - * @param source {@linkcode Pokemon} The attacking Pokémon. - * @param move {@linkcode Move} The move being used by the attacking Pokémon. - * @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). - * @param simulated Whether to apply abilities via simulated calls (defaults to `true`) - * @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity. - * @param useIllusion - Whether we want the attack move effectiveness on the illusion or not + * Calculate the effectiveness of the move against this Pokémon, including + * modifiers from move and ability attributes + * @param source - The attacking Pokémon. + * @param move - The move being used by the attacking Pokémon. + * @param ignoreAbility - Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). + * @param simulated - Whether to apply abilities via simulated calls (defaults to `true`) + * @param cancelled - Stores whether the move was cancelled by a non-type-based immunity. + * @param useIllusion - Whether to consider an active illusion * @returns The type damage multiplier, indicating the effectiveness of the move */ getMoveEffectiveness( @@ -2540,10 +2580,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Computes the given Pokemon's matchup score against this Pokemon. + * Compute the given Pokémon's matchup score against this Pokémon + * @remarks * In most cases, this score ranges from near-zero to 16, but the maximum possible matchup score is 64. - * @param opponent {@linkcode Pokemon} The Pokemon to compare this Pokemon against - * @returns A score value based on how favorable this Pokemon is when fighting the given Pokemon + * @param opponent - The Pokemon to compare this Pokémon against + * @returns A score value based on how favorable this Pokémon is when fighting the given Pokémon */ getMatchupScore(opponent: Pokemon): number { const enemyTypes = opponent.getTypes(true, false, false, true); @@ -2620,6 +2661,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return (atkScore + defScore) * Math.min(hpDiffRatio, 1); } + /** + * Get the first evolution this Pokémon meets the conditions to evolve into + * @remarks + * Fusion evolutions are also considered. + * @returns The evolution this pokemon can currently evolve into, or `null` if it cannot evolve + */ getEvolution(): SpeciesFormEvolution | null { if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) { const evolutions = pokemonEvolutions[this.species.speciesId]; @@ -2645,11 +2692,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets all level up moves in a given range for a particular pokemon. - * @param startingLevel Don't include moves below this level - * @param includeEvolutionMoves Whether to include evolution moves - * @param simulateEvolutionChain Whether to include moves from prior evolutions - * @param includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves + * Get all level up moves in a given range for a particular pokemon. + * @param startingLevel - Don't include moves below this level + * @param includeEvolutionMoves - Whether to include evolution moves + * @param simulateEvolutionChain - Whether to include moves from prior evolutions + * @param includeRelearnerMoves - Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves * @returns A list of moves and the levels they can be learned at */ getLevelMoves( @@ -2781,12 +2828,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Helper function for getLevelMoves. + * Helper function for getLevelMoves + * + * @remarks * Finds all non-duplicate items from the input, and pushes them into the output. * Two items count as duplicate if they have the same Move, regardless of level. * - * @param levelMoves the input array to search for non-duplicates from - * @param ret the output array to be pushed into. + * @param levelMoves - The input array to search for non-duplicates from + * @param ret - The output array to be pushed into. */ private static getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void { const uniqueMoves: MoveId[] = []; @@ -2800,13 +2849,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Get a list of all egg moves - * * @returns list of egg moves */ getEggMoves(): MoveId[] | undefined { return speciesEggMoves[this.getSpeciesForm().getRootSpeciesId()]; } + /** + * Create a new {@linkcode PokemonMove} and set it to the specified move index in this Pokémon's moveset. + * @param moveIndex - The index of the move to set + * @param moveId - The ID of the move to set + */ setMove(moveIndex: number, moveId: MoveId): void { if (moveId === MoveId.NONE) { return; @@ -2819,14 +2872,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon shiny based on the trainer's trainer ID and secret ID. + * Attempt to set the Pokémon's shininess based on the trainer's trainer ID and secret ID. * Endless Pokemon in the end biome are unable to be set to shiny * + * @remarks + * * The exact mechanic is that it calculates E as the XOR of the player's trainer ID and secret ID. * F is calculated as the XOR of the first 16 bits of the Pokemon's ID with the last 16 bits. * The XOR of E and F are then compared to the {@linkcode shinyThreshold} (or {@linkcode thresholdOverride} if set) to see whether or not to generate a shiny. * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536 - * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param thresholdOverride - number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @returns true if the Pokemon has been set as a shiny, false otherwise */ trySetShiny(thresholdOverride?: number): boolean { @@ -2867,14 +2922,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon shiny based on seed. + * Tries to set a Pokémon's shininess based on seed + * + * @remarks * For manual use only, usually to roll a Pokemon's shiny chance a second time. * If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck. * * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` - * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} - * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise + * @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} + * @returns Whether this Pokémon was set to shiny */ public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { if (!this.shiny) { @@ -2900,11 +2957,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a shiny variant - * @returns `0-2`, with the following probabilities: - * - Has a 10% chance of returning `2` (epic variant) - * - Has a 30% chance of returning `1` (rare variant) - * - Has a 60% chance of returning `0` (basic shiny) + * Randomly generate a shiny variant + * + * @remarks + * Variants are returned with the following probabilities: + * + * | Variant | Description | Probability | + * |---------|----------------|-------------| + * | 0 | Basic shiny | 60% | + * | 1 | Rare variant | 30% | + * | 2 | Epic variant | 10% | + * + * @returns The randomly chosen shiny variant */ protected generateShinyVariant(): Variant { const formIndex: number = this.formIndex; @@ -2940,12 +3004,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon to have its hidden ability based on seed, if it exists. + * Function that tries to set this Pokemon to have its hidden ability based on seed, if it exists. + * + * @remarks * For manual use only, usually to roll a Pokemon's hidden ability chance a second time. * * The base hidden ability odds are {@linkcode BASE_HIDDEN_ABILITY_CHANCE} / `65536` - * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} + * @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) + * @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} * @returns `true` if the Pokemon has been set to have its hidden ability, `false` otherwise */ public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { @@ -2964,6 +3030,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.abilityIndex === 2; } + /** + * Generate a fusion species and add it to this Pokémon + * @param forStarter - Whether this fusion is being generated for a starter Pokémon; default `false` + */ public generateFusionSpecies(forStarter?: boolean): void { const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { @@ -3030,6 +3100,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.generateName(); } + /** Remove the fusion species from this Pokémon */ public clearFusionSpecies(): void { this.fusionSpecies = null; this.fusionFormIndex = 0; @@ -3044,7 +3115,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - /** Generates a semi-random moveset for a Pokemon */ + /** Generate a semi-random moveset for this Pokémon */ public generateAndPopulateMoveset(): void { generateMoveset(this); @@ -3063,6 +3134,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return move?.isUsable(this, ignorePp) ?? false; } + /** Show this Pokémon's info panel */ showInfo(): void { if (!this.battleInfo.visible) { const otherBattleInfo = globalScene.fieldUI @@ -3091,7 +3163,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - hideInfo(): Promise { + /** Hide this Pokémon's info panel */ + async hideInfo(): Promise { return new Promise(resolve => { if (this.battleInfo?.visible) { globalScene.tweens.add({ @@ -3115,14 +3188,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - /** - * sets if the pokemon is switching out (if it's a enemy wild implies it's going to flee) - * @param status - boolean - */ - setSwitchOutStatus(status: boolean): void { - this.switchOutStatus = status; - } - updateInfo(instant?: boolean): Promise { return this.battleInfo.updateInfo(this, instant); } @@ -3133,8 +3198,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Adds experience to this PlayerPokemon, subject to wave based level caps. - * @param exp The amount of experience to add - * @param ignoreLevelCap Whether to ignore level caps when adding experience (defaults to false) + * @param exp - The amount of experience to add + * @param ignoreLevelCap - Whether to ignore level caps when adding experience; default `false` */ addExp(exp: number, ignoreLevelCap = false) { const maxExpLevel = globalScene.getMaxExpLevel(ignoreLevelCap); @@ -3151,8 +3216,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Compares if `this` and {@linkcode target} are on the same team. - * @param target the {@linkcode Pokemon} to compare against. + * Check whether the specified Pokémon is an opponent + * @param target - The {@linkcode Pokemon} to compare against * @returns `true` if the two pokemon are allies, `false` otherwise */ public isOpponent(target: Pokemon): boolean { @@ -3197,17 +3262,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the stat stage multiplier of the user against an opponent. + * Calculate the stat stage multiplier of the user against an opponent * - * Note that this does not apply to evasion or accuracy + * @remarks + * This does not apply to evasion or accuracy * @see {@linkcode getAccuracyMultiplier} * @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` + * @param ignoreOppAbility - determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored; default `false` + * @param isCritical - determines whether a critical hit has occurred or not; default `false` + * @param simulated - determines whether effects are applied without altering game state; default `true` + * @param ignoreHeldItems - determines whether this Pokemon's held items should be ignored during the stat calculation; default `false` * @returns the stat stage multiplier to be used for effective stat calculation */ getStatStageMultiplier( @@ -3344,15 +3410,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the base damage of the given move against this Pokemon when attacked by the given source. * Used during damage calculation and for Shell Side Arm's forecasting effect. - * @param source - The attacking {@linkcode Pokemon}. - * @param move - The {@linkcode Move} used in the attack. - * @param moveCategory - The move's {@linkcode MoveCategory} after variable-category effects are applied. - * @param ignoreAbility - If `true`, ignores this Pokemon's defensive ability effects (defaults to `false`). - * @param ignoreSourceAbility - If `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). - * @param ignoreAllyAbility - If `true`, ignores the ally Pokemon's ability effects (defaults to `false`). - * @param ignoreSourceAllyAbility - If `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`). - * @param isCritical - if `true`, calculates effective stats as if the hit were critical (defaults to `false`). - * @param simulated - if `true`, suppresses changes to game state during calculation (defaults to `true`). + * @param __namedParameters.source - Needed for proper typedoc rendering * @returns The move's base damage against this Pokemon when used by the source Pokemon. */ getBaseDamage({ @@ -3470,15 +3528,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the damage of an attack made by another Pokemon against this Pokemon - * @param source {@linkcode Pokemon} the attacking Pokemon - * @param move The {@linkcode Move} used in the attack - * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects - * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects - * @param ignoreAllyAbility If `true`, ignores the ally Pokemon's ability effects - * @param ignoreSourceAllyAbility If `true`, ignores the ability effects of the attacking pokemon's ally - * @param isCritical If `true`, calculates damage for a critical hit. - * @param simulated If `true`, suppresses changes to game state during the calculation. - * @param effectiveness If defined, used in place of calculated effectiveness values + * @param __namedParameters.source - Needed for proper typedoc rendering * @returns The {@linkcode DamageCalculationResult} * @todo Condense various multipliers into a single function */ @@ -3808,12 +3858,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Called by damageAndUpdate() - * @param damage integer - * @param ignoreSegments boolean, not currently used - * @param preventEndure used to update damage if endure or sturdy - * @param ignoreFaintPhase flag on whether to add FaintPhase if pokemon after applying damage faints - * @returns integer representing damage dealt + * Submethod called by {@linkcode damageAndUpdate} to apply damage to this Pokemon and adjust its HP. + * @param damage - The damage to deal + * @param _ignoreSegments - Whether to ignore boss segments; default `false` + * @param preventEndure - Whether to allow the damage to bypass an Endure/Sturdy effect + * @param ignoreFaintPhase - Whether to ignore adding a FaintPhase if this damage causes a faint + * @returns The actual damage dealt */ damage(damage: number, _ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { if (this.isFainted()) { @@ -3859,14 +3909,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Given the damage, adds a new DamagePhase and update HP values, etc. * - * Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly - * @param damage integer - passed to damage() - * @param result an enum if it's super effective, not very, etc. - * @param isCritical boolean if move is a critical hit - * @param ignoreSegments boolean, passed to damage() and not used currently - * @param preventEndure boolean, ignore endure properties of pokemon, passed to damage() - * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() - * @returns integer of damage done + * @remarks + * Checks for {@linkcode HitResult.INDIRECT | Indirect} hits to account for Endure/Reviver Seed applying correctly + * @param damage - The damage to inflict on this Pokémon + * @param __namedParameters.source - Needed for proper typedoc rendering + * @returns Amount of damage actually done */ damageAndUpdate( damage: number, @@ -3877,10 +3924,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ignoreFaintPhase = false, source, }: { + /** + * An enum if it's super effective, not very effective, etc; default {@linkcode HitResult.EFFECTIVE} + */ result?: DamageResult; + /** Whether the attack was a critical hit */ isCritical?: boolean; + /** Whether to ignore boss segments */ ignoreSegments?: boolean; + /** Whether to ignore adding a FaintPhase if this damage causes a faint; default `false` */ ignoreFaintPhase?: boolean; + /** The Pokémon inflicting the damage, or undefined if not caused by a Pokémon */ source?: Pokemon; } = {}, ): number { @@ -3914,17 +3968,25 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return damage; } - heal(amount: number): number { + /** + * Restore a specific amount of HP to this Pokémon + * @param amount - The amount of HP to restore + * @returns The true amount of HP restored; may be less than `amount` if `amount` would overheal + */ + public heal(amount: number): number { const healAmount = Math.min(amount, this.getMaxHp() - this.hp); this.hp += healAmount; return healAmount; } - isBossImmune(): boolean { + public isBossImmune(): boolean { return this.isBoss(); } - isMax(): boolean { + /** + * @returns Whether this Pokémon is in a Dynamax or Gigantamax form + */ + public isMax(): boolean { const maxForms = [ SpeciesFormKey.GIGANTAMAX, SpeciesFormKey.GIGANTAMAX_RAPID, @@ -3936,7 +3998,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - isMega(): boolean { + /** + * @returns Whether this Pokémon is in a Mega or Primal form + */ + public isMega(): boolean { const megaForms = [ SpeciesFormKey.MEGA, SpeciesFormKey.MEGA_X, @@ -3949,7 +4014,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - canAddTag(tagType: BattlerTagType): boolean { + /** + * Check whether a battler tag can be added to this Pokémon + * + * @param tagType - The tag to check + * @returns - Whether the tag can be added + * @see {@linkcode addTag} + */ + public canAddTag(tagType: BattlerTagType): boolean { if (this.getTag(tagType)) { return false; } @@ -3973,7 +4045,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return !cancelled.value; } - addTag(tagType: BattlerTagType, turnCount = 0, sourceMove?: MoveId, sourceId?: number): boolean { + /** + * Add a new {@linkcode BattlerTag} of the specified `tagType` + * + * @remarks + * Also ensures the tag is able to be applied, similar to {@linkcode canAddTag} + * + * @param tagType - The type of tag to add + * @param turnCount - The number of turns the tag should last; default `0` + * @param sourceMove - The id of the move that causing the tag to be added, if caused by a move + * @param sourceId - The {@linkcode Pokemon#id | id} of the pokemon causing the tag to be added, if caused by a Pokémon + * @returns Whether the tag was successfully added + * @see {@linkcode canAddTag} + */ + public addTag(tagType: BattlerTagType, turnCount = 0, sourceMove?: MoveId, sourceId?: number): boolean { const existingTag = this.getTag(tagType); if (existingTag) { existingTag.onOverlap(this); @@ -3982,6 +4067,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const newTag = getBattlerTag(tagType, turnCount, sourceMove!, sourceId!); // TODO: are the bangs correct? + // TODO: Just call canAddTag() here? Can possibly overload it to accept an actual tag instead of just a type const cancelled = new BooleanHolder(false); applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: newTag, cancelled }); if (cancelled.value) { @@ -4004,11 +4090,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; - getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; - getTag(tagType: BattlerTagType): BattlerTag | undefined; - getTag(tagType: Constructor): T | undefined; - getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { + // TODO: Utilize a type map for these so we can avoid overloads + public getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; + public getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; + public getTag(tagType: BattlerTagType): BattlerTag | undefined; + public getTag(tagType: Constructor): T | undefined; + public getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { return typeof tagType === "function" ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType); @@ -4016,21 +4103,35 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { findTag(tagFilter: (tag: BattlerTag) => tag is T): T | undefined; findTag(tagFilter: (tag: BattlerTag) => boolean): BattlerTag | undefined; - findTag(tagFilter: (tag: BattlerTag) => boolean) { - return this.summonData.tags.find(t => tagFilter(t)); + /** + * Find the first `BattlerTag` matching the specified predicate + * @remarks + * Equivalent to `this.summonData.tags.find(tagFilter)`. + * @param tagFilter - The predicate to match against + * @returns The first matching tag, or `undefined` if none match + */ + public findTag(tagFilter: (tag: BattlerTag) => boolean) { + return this.summonData.tags.find(tagFilter); } - findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { - return this.summonData.tags.filter(t => tagFilter(t)); + /** + * Return the list of `BattlerTag`s that satisfy the given predicate + * @remarks + * Equivalent to `this.summonData.tags.filter(tagFilter)`. + * @param tagFilter - The predicate to match against + * @returns The filtered list of tags + */ + public findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { + return this.summonData.tags.filter(tagFilter); } /** * Tick down the first {@linkcode BattlerTag} found matching the given {@linkcode BattlerTagType}, * removing it if its duration goes below 0. - * @param tagType the {@linkcode BattlerTagType} to check against - * @returns `true` if the tag was present + * @param tagType - The `BattlerTagType` to lapse + * @returns Whether the tag was present */ - lapseTag(tagType: BattlerTagType): boolean { + public lapseTag(tagType: BattlerTagType): boolean { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (!tag) { @@ -4045,11 +4146,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Tick down all {@linkcode BattlerTags} matching the given {@linkcode BattlerTagLapseType}, - * removing any whose durations fall below 0. - * @param tagType the {@linkcode BattlerTagLapseType} to tick down + * Tick down all {@linkcode BattlerTags} that lapse on the provided + * `lapseType`, removing any whose durations fall below 0. + * @param lapseType - The type of lapse to process */ - lapseTags(lapseType: BattlerTagLapseType): void { + public lapseTags(lapseType: BattlerTagLapseType): void { const tags = this.summonData.tags; tags .filter( @@ -4064,10 +4165,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Remove the first tag matching the given {@linkcode BattlerTagType}. - * @param tagType the {@linkcode BattlerTagType} to search for and remove + * Remove the first tag matching `tagType` and invoke its + * {@linkcode BattlerTag#onRemove | onRemove} method. + * @remarks + * Only removes the first matching tag, if multiple are present; to remove all + * matching tags, use {@linkcode findAndRemoveTags} instead. + * @param tagType - The tag type to search for and remove */ - removeTag(tagType: BattlerTagType): void { + public removeTag(tagType: BattlerTagType): void { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag) { @@ -4077,10 +4182,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Find and remove all {@linkcode BattlerTag}s matching the given function. - * @param tagFilter a function dictating which tags to remove + * Find and remove all {@linkcode BattlerTag}s matching the given function and + * invoke their {@linkcode BattlerTag#onRemove | onRemove} methods. + * @remarks + * Removes all matching tags; to remove only the first matching tag, use + * {@linkcode removeTag} instead. + * @param tagFilter - A function dictating which tags to remove */ - findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { + public findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { const tags = this.summonData.tags; const tagsToRemove = tags.filter(t => tagFilter(t)); for (const tag of tagsToRemove) { @@ -4090,11 +4199,22 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - removeTagsBySourceId(sourceId: number): void { + /** + * Remove all tags that were applied by a Pokémon with the given `sourceId`, + * invoking their {@linkcode BattlerTag#onRemove | onRemove} methods. + * @param sourceId - Tags with this {@linkcode Pokemon#id | id} as their {@linkcode BattlerTag#sourceId | sourceId} will be removed + * @see {@linkcode findAndRemoveTags} + */ + public removeTagsBySourceId(sourceId: number): void { this.findAndRemoveTags(t => t.isSourceLinked() && t.sourceId === sourceId); } - transferTagsBySourceId(sourceId: number, newSourceId: number): void { + /** + * Change the `sourceId` of all tags on this Pokémon with the given `sourceId` to `newSourceId`. + * @param sourceId - The {@linkcode Pokemon#id | id} of the pokemon whose tags are to be transferred + * @param newSourceId - The {@linkcode Pokemon#id | id} of the pokemon to which the tags are being transferred + */ + public transferTagsBySourceId(sourceId: number, newSourceId: number): void { this.summonData.tags.forEach(t => { if (t.sourceId === sourceId) { t.sourceId = newSourceId; @@ -4103,11 +4223,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Transferring stat changes and Tags - * @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass + * Transfer stat changes and Tags from another Pokémon + * + * @remarks + * Used to implement Baton Pass and switching via the Baton item. + * + * @param source - The pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass */ - transferSummon(source: Pokemon): void { - // Copy all stat stages + public transferSummon(source: Pokemon): void { for (const s of BATTLE_STATS) { const sourceStage = source.getStatStage(s); if (this.isPlayer() && sourceStage === 6) { @@ -4137,9 +4260,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets whether the given move is currently disabled for this Pokemon. + * Get whether the given move is currently disabled for this Pokémon * - * @param moveId - The {@linkcode MoveId} ID of the move to check + * @param moveId - The ID of the move to check * @returns `true` if the move is disabled for this Pokemon, otherwise `false` * * @see {@linkcode MoveRestrictionBattlerTag} @@ -4149,9 +4272,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets whether the given move is currently disabled for the user based on the player's target selection + * Get whether the given move is currently disabled for the user based on the player's target selection * - * @param moveId - The {@linkcode MoveId} ID of the move to check + * @param moveId - The ID of the move to check * @param user - The move user * @param target - The target of the move * @@ -4169,11 +4292,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. + * Get the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. * - * @param moveId - {@linkcode MoveId} ID of the move to check - * @param user - {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status - * @param target - {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status + * @param moveId - The ID of the move to check + * @param user - The move user, optional and used when the target is a factor in the move's restricted status + * @param target - The target of the move; optional, and used when the target is a factor in the move's restricted status * @returns The first tag on this Pokemon that restricts the move, or `null` if the move is not restricted. */ getRestrictingTag(moveId: MoveId, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null { @@ -4201,7 +4324,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Add a move to the end of this {@linkcode Pokemon}'s move history, * used to record its most recently executed actions. - * @param turnMove - The {@linkcode TurnMove} to add + * @param turnMove - The move to add to the history */ public pushMoveHistory(turnMove: TurnMove): void { if (!this.isOnField()) { @@ -4220,7 +4343,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns An array of {@linkcode TurnMove}, as specified above. */ // TODO: Update documentation in dancer PR to mention "getLastNonVirtualMove" - getLastXMoves(moveCount = 1): TurnMove[] { + public getLastXMoves(moveCount = 1): TurnMove[] { const moveHistory = this.getMoveHistory(); if (moveCount > 0) { return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse(); @@ -4238,7 +4361,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns The last move this Pokemon has used satisfying the aforementioned conditions, * or `undefined` if no applicable moves have been used since switching in. */ - getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { + public getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { return this.getLastXMoves(-1).find( m => m.move !== MoveId.NONE @@ -4251,7 +4374,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Return this Pokemon's move queue, consisting of all the moves it is slated to perform. * @returns An array of {@linkcode TurnMove}, as described above */ - getMoveQueue(): TurnMove[] { + public getMoveQueue(): TurnMove[] { return this.summonData.moveQueue; } @@ -4259,11 +4382,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Add a new entry to the end of this Pokemon's move queue. * @param queuedMove - A {@linkcode TurnMove} to push to this Pokemon's queue. */ - pushMoveQueue(queuedMove: TurnMove): void { + public pushMoveQueue(queuedMove: TurnMove): void { this.summonData.moveQueue.push(queuedMove); } - changeForm(formChange: SpeciesFormChange): Promise { + /** + * Change this Pokémon's form to the specified form, loading the required + * assets and updating its stats and info display. + * @param formChange - The form to change to + * @returns A Promise that resolves once the form change has completed. + */ + public async changeForm(formChange: SpeciesFormChange): Promise { return new Promise(resolve => { this.formIndex = Math.max( this.species.forms.findIndex(f => f.formKey === formChange.formKey), @@ -4285,7 +4414,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null { + /** + * Play this Pokémon's cry sound + * @param soundConfig - Optional sound configuration to apply to the cry + * @param sceneOverride - Optional scene to use instead of the global scene + */ + public cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null { const scene = sceneOverride ?? globalScene; // TODO: is `sceneOverride` needed? const cry = this.getSpeciesForm(undefined, true).cry(soundConfig); if (!cry) { @@ -4324,8 +4458,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return cry; } - // biome-ignore lint: there are a ton of issues.. - faintCry(callback: Function): void { + /** + * Play this Pokémon's faint cry, pausing its animation until the cry is finished. + * @param callback - A function to be called once the cry has finished playing + */ + public faintCry(callback: () => any): void { if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) { this.fusionFaintCry(callback); return; @@ -4397,8 +4534,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - // biome-ignore lint/complexity/noBannedTypes: Consider refactoring to change type of Function - private fusionFaintCry(callback: Function): void { + /** + * Play this Pokémon's fusion faint cry, which is a mixture of the faint cries + * for both of its species + * @param callback - A function to be called once the cry has finished playing + */ + private fusionFaintCry(callback: () => any): void { const key = this.species.getCryKey(this.formIndex); let i = 0; let rate = 0.85; @@ -4508,7 +4649,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - isOppositeGender(pokemon: Pokemon): boolean { + /** + * Check the specified pokemon is considered to be the opposite gender as this pokemon + * @param pokemon - The Pokémon to compare against + * @returns Whether the pokemon are considered to be opposite genders + */ + public isOppositeGender(pokemon: Pokemon): boolean { return ( this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE) @@ -4518,12 +4664,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Display an immunity message for a failed status application. * @param quiet - Whether to suppress message and return early - * @param reason - The reason for the status application failure - + * @param reason - The reason for the status application failure; * can be "overlap" (already has same status), "other" (generic fail message) * or a {@linkcode TerrainType} for terrain-based blockages. * Default `"other"` */ - queueStatusImmuneMessage( + public queueStatusImmuneMessage( quiet: boolean, reason: "overlap" | "other" | Exclude = "other", ): void { @@ -4750,7 +4896,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: Exclude): void; + public doSetStatus(effect: Exclude): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - {@linkcode StatusEffect.SLEEP} @@ -4760,7 +4906,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; + public doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - The {@linkcode StatusEffect} to set @@ -4771,7 +4917,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; + public doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - The {@linkcode StatusEffect} to set @@ -4783,7 +4929,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. * @todo Make this and all related fields private and change tests to use a field-based helper or similar */ - doSetStatus( + public doSetStatus( effect: StatusEffect, sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4), ): void { @@ -4832,11 +4978,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Resets the status of a pokemon. - * @param revive Whether revive should be cured; defaults to true. - * @param confusion Whether resetStatus should include confusion or not; defaults to false. - * @param reloadAssets Whether to reload the assets or not; defaults to false. - * @param asPhase Whether to reset the status in a phase or immediately + * Reset this Pokémon's status + * @param revive - Whether revive should be cured; default `true` + * @param confusion - Whether to also cure confusion; default `false` + * @param reloadAssets - Whether to reload the assets or not; default `false` + * @param asPhase - Whether to reset the status in a phase or immediately; default `true` */ resetStatus(revive = true, confusion = false, reloadAssets = false, asPhase = true): void { const lastStatus = this.status?.effect; @@ -4852,9 +4998,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Performs the action of clearing a Pokemon's status - * + * Perform the action of clearing a Pokemon's status + * @remarks * This is a helper to {@linkcode resetStatus}, which should be called directly instead of this method + * @param confusion - Whether to also clear this Pokémon's confusion + * @param reloadAssets - Whether to reload this pokemon's assets */ public clearStatus(confusion: boolean, reloadAssets: boolean) { const lastStatus = this.status?.effect; @@ -4873,11 +5021,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks if this Pokemon is protected by Safeguard - * @param attacker the {@linkcode Pokemon} inflicting status on this Pokemon - * @returns `true` if this Pokemon is protected by Safeguard; `false` otherwise. + * Check if this Pokémon is protected by Safeguard + * @param attacker - The Pokémon responsible for the interaction that needs to check against Safeguard + * @returns Whether this Pokémon is protected by Safeguard */ - isSafeguarded(attacker: Pokemon): boolean { + public isSafeguarded(attacker: Pokemon): boolean { const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (globalScene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { const bypassed = new BooleanHolder(false); @@ -4890,11 +5038,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Performs miscellaneous setup for when the Pokemon is summoned, like generating the substitute sprite + * Perform miscellaneous setup for when the Pokemon is summoned, like generating the substitute sprite * @param resetSummonData - Whether to additionally reset the Pokemon's summon data (default: `false`) */ public fieldSetup(resetSummonData?: boolean): void { - this.setSwitchOutStatus(false); + this.switchOutStatus = false; if (globalScene) { globalScene.triggerPokemonFormChange(this, SpeciesFormChangePostMoveTrigger, true); } @@ -4922,7 +5070,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Reset this Pokemon's {@linkcode PokemonSummonData | SummonData} and {@linkcode PokemonTempSummonData | TempSummonData} * in preparation for switching pokemon, as well as removing any relevant on-switch tags. */ - resetSummonData(): void { + public resetSummonData(): void { const illusion: IllusionData | null = this.summonData.illusion; if (this.summonData.speciesForm) { this.summonData.speciesForm = null; @@ -4935,17 +5083,21 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Reset a {@linkcode Pokemon}'s per-battle {@linkcode PokemonBattleData | battleData}, + * Reset this Pokémon's per-battle {@linkcode PokemonBattleData | battleData} * as well as any transient {@linkcode PokemonWaveData | waveData} for the current wave. + * + * @remarks * Should be called once per arena transition (new biome/trainer battle/Mystery Encounter). */ - resetBattleAndWaveData(): void { + public resetBattleAndWaveData(): void { this.battleData = new PokemonBattleData(); this.resetWaveData(); } /** - * Reset a {@linkcode Pokemon}'s {@linkcode PokemonWaveData | waveData}. + * Reset this Pokémon's {@linkcode PokemonWaveData | waveData} + * + * @remarks * Should be called upon starting a new wave in addition to whenever an arena transition occurs. * @see {@linkcode resetBattleAndWaveData} */ @@ -4954,6 +5106,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.tempSummonData.waveTurnCount = 1; } + /** + * Reset this Pokémon's Terastallization state + * + * @remarks + * Responsible for all of the cleanup required when a pokemon goes from being + * terastallized to no longer terastallized: + * - Resetting stellar type boosts + * - Updating the Pokémon's terastallization-dependent form + * - Adjusting the sprite pipeline to remove the Tera effect + */ resetTera(): void { const wasTerastallized = this.isTerastallized; this.isTerastallized = false; @@ -4964,6 +5126,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** + * Clear this Pokémon's transient turn data + */ resetTurnData(): void { this.turnData = new PokemonTurnData(); } @@ -4973,6 +5138,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return (this.getSpeciesForm().getBaseExp() * this.level) / 5 + 1; } + //#region Sprite and Animation Methods setFrameRate(frameRate: number) { globalScene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate; try { @@ -5049,6 +5215,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** Play the shiny sparkle animation and effects, if applicable */ sparkle(): void { if (this.shinySparkle) { doShinySparkleAnim(this.shinySparkle, this.variant); @@ -5380,15 +5547,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionCanvas.remove(); } + //#endregion Sprite and Animation Methods + /** - * Generates a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * Generate a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * + * @remarks * This calls either {@linkcode BattleScene.randBattleSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle-scene.ts` * which calls {@linkcode Battle.randSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle.ts` * which calls {@linkcode randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts`, * or it directly calls {@linkcode randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts` if there is no current battle * - * @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} - * @param min The minimum integer to pick, default `0` + * @param range - How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} + * @param min - The minimum integer to pick; default `0` * @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1) */ randBattleSeedInt(range: number, min = 0): number { @@ -5396,10 +5567,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy - * @param min The minimum integer to generate - * @param max The maximum integer to generate - * @returns a random integer between {@linkcode min} and {@linkcode max} inclusive + * Generate a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * @param min - The minimum integer to generate + * @param max - The maximum integer to generate + * @returns A random integer between {@linkcode min} and {@linkcode max} (inclusive) */ randBattleSeedIntRange(min: number, max: number): number { return globalScene.currentBattle ? globalScene.randBattleSeedInt(max - min + 1, min) : randSeedIntRange(min, max); @@ -5407,9 +5578,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Causes a Pokemon to leave the field (such as in preparation for a switch out/escape). - * @param clearEffects Indicates if effects should be cleared (true) or passed - * to the next pokemon, such as during a baton pass (false) - * @param hideInfo Indicates if this should also play the animation to hide the Pokemon's + * @param clearEffects - Indicates if effects should be cleared (true) or passed + * to the next pokemon, such as during a baton pass (false) + * @param hideInfo - Indicates if this should also play the animation to hide the Pokemon's * info container. */ leaveField(clearEffects = true, hideInfo = true, destroy = false) { @@ -5429,25 +5600,33 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } // Trigger abilities that activate upon leaving the field applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: this }); - this.setSwitchOutStatus(true); + this.switchOutStatus = true; globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); globalScene.field.remove(this, destroy); } + /** + * @inheritdoc {@linkcode Phaser.GameObjects.Container#destroy} + * + * ### Custom Behavior + * In addition to the base `destroy` behavior, this also destroys the Pokemon's + * {@linkcode battleInfo} and substitute sprite (as applicable). + */ destroy(): void { this.battleInfo?.destroy(); this.destroySubstitute(); super.destroy(); } + // TODO: Turn this into a getter getBattleInfo(): BattleInfo { return this.battleInfo; } /** - * Checks whether or not the Pokemon's root form has the same ability - * @param abilityIndex the given ability index we are checking - * @returns true if the abilities are the same + * Check whether or not this Pokémon's root form has the same ability + * @param abilityIndex - The ability index to check + * @returns Whether the Pokemon's root form has the same ability */ hasSameAbilityInRootForm(abilityIndex: number): boolean { const currentAbilityIndex = this.abilityIndex; @@ -5456,9 +5635,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Helper function to check if the player already owns the starter data of the Pokemon's + * Helper function to check if the player already owns the starter data of this Pokémon's * current ability - * @param ownedAbilityAttrs the owned abilityAttr of this Pokemon's root form + * @param ownedAbilityAttrs - The owned abilityAttr of this Pokemon's root form * @returns true if the player already has it, false otherwise */ checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs: number): boolean { @@ -5499,8 +5678,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Record a berry being eaten for ability and move triggers. * Only tracks things that proc _every_ time a berry is eaten. - * @param berryType The type of berry being eaten. - * @param updateHarvest Whether to track the berry for harvest; default `true`. + * @param berryType - The type of berry being eaten. + * @param updateHarvest - Whether to track the berry for harvest; default `true`. */ public recordEatenBerry(berryType: BerryType, updateHarvest = true) { this.battleData.hasEatenBerry = true; @@ -5511,6 +5690,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.turnData.berriesEaten.push(berryType); } + /** + * Get the number of persistent treasure items this Pokemon has + * @remarks + * Persistent treasure items are defined as held items that give money + * after battle, such as the Lucky Egg or the Amulet Coin. + * Used exclusively for Gimmighoul's evolution condition + * @returns The number of persistent treasure items this Pokémon has + */ getPersistentTreasureCount(): number { return ( this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length @@ -5641,10 +5828,10 @@ export class PlayerPokemon extends Pokemon { } /** - * Causes this mon to leave the field (via {@linkcode leaveField}) and then - * opens the party switcher UI to switch a new mon in - * @param switchType the {@linkcode SwitchType} for this switch-out. If this is - * `BATON_PASS` or `SHED_TAIL`, this Pokemon's effects are not cleared upon leaving + * Cause this Pokémon to leave the field (via {@linkcode leaveField}) and then + * open the party switcher UI to switch in a new Pokémon + * @param switchType - The type of this switch-out. If this is + * `BATON_PASS` or `SHED_TAIL`, this Pokémon's effects are not cleared upon leaving * the field. */ switchOut(switchType: SwitchType = SwitchType.SWITCH): Promise { @@ -5978,8 +6165,8 @@ export class PlayerPokemon extends Pokemon { } /** - * Returns a Promise to fuse two PlayerPokemon together - * @param pokemon The PlayerPokemon to fuse to this one + * Fuse another PlayerPokemon into this one + * @param pokemon - The PlayerPokemon to fuse to this one */ fuse(pokemon: PlayerPokemon): void { this.fusionSpecies = pokemon.species; @@ -6470,7 +6657,7 @@ export class EnemyPokemon extends Pokemon { /** * Determines the Pokemon the given move would target if used by this Pokemon - * @param moveId {@linkcode MoveId} The move to be used + * @param moveId - The move to be used * @returns The indexes of the Pokemon the given move would target */ getNextTargets(moveId: MoveId): BattlerIndex[] { @@ -6583,7 +6770,11 @@ export class EnemyPokemon extends Pokemon { return 0; } - damage(damage: number, ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { + /** + * @inheritdoc + * @param ignoreSegments - Whether to ignore boss segments when applying damage + */ + public damage(damage: number, ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { if (this.isFainted()) { return 0; } @@ -6639,7 +6830,7 @@ export class EnemyPokemon extends Pokemon { return ret; } - canBypassBossSegments(segmentCount = 1): boolean { + private canBypassBossSegments(segmentCount = 1): boolean { if ( globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && !this.formIndex @@ -6653,10 +6844,12 @@ export class EnemyPokemon extends Pokemon { /** * Go through a boss' health segments and give stats boosts for each newly cleared segment + * + * @remarks * The base boost is 1 to a random stat that's not already maxed out per broken shield * For Pokemon with 3 health segments or more, breaking the last shield gives +2 instead * For Pokemon with 5 health segments or more, breaking the last two shields give +2 each - * @param segmentIndex index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) + * @param segmentIndex - index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) */ handleBossSegmentCleared(segmentIndex: number): void { let doStatBoost = !this.hasTrainer(); @@ -6718,22 +6911,22 @@ export class EnemyPokemon extends Pokemon { } } - getFieldIndex(): number { + public getFieldIndex(): number { return globalScene.getEnemyField().indexOf(this); } - getBattlerIndex(): BattlerIndex { + public getBattlerIndex(): BattlerIndex { return BattlerIndex.ENEMY + this.getFieldIndex(); } /** * Add a new pokemon to the player's party (at `slotIndex` if set). * The new pokemon's visibility will be set to `false`. - * @param pokeballType the type of pokeball the pokemon was caught with - * @param slotIndex an optional index to place the pokemon in the party - * @returns the pokemon that was added or null if the pokemon could not be added + * @param pokeballType - The type of pokeball the pokemon was caught with + * @param slotIndex - An optional index to place the pokemon in the party + * @returns The pokemon that was added or null if the pokemon could not be added */ - addToParty(pokeballType: PokeballType, slotIndex = -1) { + public addToParty(pokeballType: PokeballType, slotIndex = -1) { const party = globalScene.getPlayerParty(); let ret: PlayerPokemon | null = null; @@ -6776,11 +6969,11 @@ export class EnemyPokemon extends Pokemon { * Show or hide the type effectiveness multiplier window * Passing undefined will hide the window */ - updateEffectiveness(effectiveness?: string) { + public updateEffectiveness(effectiveness?: string) { this.battleInfo.updateEffectiveness(effectiveness); } - toggleFlyout(visible: boolean): void { + public toggleFlyout(visible: boolean): void { this.battleInfo.toggleFlyout(visible); } } diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 3fdf07866d2..c804ad0f05b 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -235,7 +235,6 @@ export class PhaseManager { /** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */ private phaseQueuePrependSpliceIndex = -1; - private nextCommandPhaseQueue: Phase[] = []; /** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */ private dynamicPhaseQueues: PhasePriorityQueue[]; @@ -293,13 +292,12 @@ export class PhaseManager { /** * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false * @param phase {@linkcode Phase} the phase to add - * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue */ - pushPhase(phase: Phase, defer = false): void { + pushPhase(phase: Phase): void { if (this.getDynamicPhaseType(phase) !== undefined) { this.pushDynamicPhase(phase); } else { - (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); + this.phaseQueue.push(phase); } } @@ -326,7 +324,7 @@ export class PhaseManager { * Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index */ clearAllPhases(): void { - for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) { + for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue]) { queue.splice(0, queue.length); } this.dynamicPhaseQueues.forEach(queue => queue.clear()); @@ -627,10 +625,6 @@ export class PhaseManager { * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) */ private populatePhaseQueue(): void { - if (this.nextCommandPhaseQueue.length > 0) { - this.phaseQueue.push(...this.nextCommandPhaseQueue); - this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length); - } this.phaseQueue.push(new TurnInitPhase()); } diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 02bb3a0b968..258ddb0b624 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -64,7 +64,8 @@ export class PokemonHealPhase extends CommonAnimPhase { } const hasMessage = !!this.message; - const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0; + const canRestorePP = this.fullRestorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0); + const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0 || canRestorePP; const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag; let lastStatusEffect = StatusEffect.NONE; diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 5de068f2ae5..5f66cf91eca 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -2,6 +2,7 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { EntryHazardTag } from "#data/arena-tag"; import { MysteryEncounterPostSummonTag } from "#data/battler-tags"; +import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; import { PokemonPhase } from "#phases/pokemon-phase"; @@ -16,6 +17,9 @@ export class PostSummonPhase extends PokemonPhase { if (pokemon.status?.effect === StatusEffect.TOXIC) { pokemon.status.toxicTurnCount = 0; } + + globalScene.arena.applyTags(ArenaTagType.PENDING_HEAL, false, pokemon); + globalScene.arena.applyTags(EntryHazardTag, false, pokemon); // If this is mystery encounter and has post summon phase tag, apply post summon effects diff --git a/src/ui/containers/arena-flyout.ts b/src/ui/containers/arena-flyout.ts index ab95d1a3e7a..355f3edb293 100644 --- a/src/ui/containers/arena-flyout.ts +++ b/src/ui/containers/arena-flyout.ts @@ -285,6 +285,12 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { switch (arenaEffectChangedEvent.constructor) { case TagAddedEvent: { const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent; + + const excludedTags = [ArenaTagType.PENDING_HEAL]; + if (excludedTags.includes(tagAddedEvent.arenaTagType)) { + return; + } + const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof EntryHazardTag; let arenaEffectType: ArenaEffectType; diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 9a6f07b4afb..43e9df190aa 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,7 +1,7 @@ import "vitest"; -import type { Phase } from "#app/phase"; import type Overrides from "#app/overrides"; +import type { Phase } from "#app/phase"; import type { ArenaTag } from "#data/arena-tag"; import type { TerrainType } from "#data/terrain"; import type { AbilityId } from "#enums/ability-id"; @@ -10,10 +10,14 @@ import type { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerTagType } from "#enums/battler-tag-type"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; +import type { PositionalTag } from "#data/positional-tags/positional-tag"; import type { PositionalTagType } from "#enums/positional-tag-type"; import type { BattleStat, EffectiveStat } from "#enums/stat"; import type { WeatherType } from "#enums/weather-type"; +import type { Pokemon } from "#field/pokemon"; +import type { GameManager } from "#test/test-utils/game-manager"; import type { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; +import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag"; import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat"; import type { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag"; import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; @@ -23,175 +27,212 @@ import type { TurnMove } from "#types/turn-move"; import type { AtLeastOne } from "#types/type-helpers"; import type { toDmgValue } from "#utils/common"; import type { expect } from "vitest"; -import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag"; +// #region Boilerplate/Helpers declare module "vitest" { - interface Assertion { - // #region Generic Matchers - - /** - * Check whether an array contains EXACTLY the given items (in any order). - * - * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality - * (as opposed to full equality). - * - * @param expected - The expected contents of the array, in any order - * @see {@linkcode expect.arrayContaining} - */ - toEqualArrayUnsorted(expected: T[]): void; - - // #endregion Generic Matchers - - // #region GameManager Matchers - - /** - * Check if the {@linkcode GameManager} has shown the given message at least once in the current battle. - * @param expectedMessage - The expected message - */ - toHaveShownMessage(expectedMessage: string): void; - /** - * @param expectedPhase - The expected {@linkcode PhaseString} - */ - toBeAtPhase(expectedPhase: PhaseString): void; - // #endregion GameManager Matchers - - // #region Arena Matchers - - /** - * Check whether the current {@linkcode WeatherType} is as expected. - * @param expectedWeatherType - The expected {@linkcode WeatherType} - */ - toHaveWeather(expectedWeatherType: WeatherType): void; - - /** - * Check whether the current {@linkcode TerrainType} is as expected. - * @param expectedTerrainType - The expected {@linkcode TerrainType} - */ - toHaveTerrain(expectedTerrainType: TerrainType): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties - */ - toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedType - The {@linkcode ArenaTagType} of the desired tag - * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} - */ - toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. - * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties - */ - toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; - /** - * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. - * @param expectedType - The {@linkcode PositionalTagType} of the desired tag - * @param count - The number of instances of {@linkcode expectedType} that should be active; - * defaults to `1` and must be within the range `[0, 4]` - */ - toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; - - // #endregion Arena Matchers - - // #region Pokemon Matchers - - /** - * Check whether a {@linkcode Pokemon}'s current typing includes the given types. - * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` - * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher - */ - toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. - * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, - * or a partially filled {@linkcode TurnMove} containing the desired properties to check - * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` - * @see {@linkcode Pokemon.getLastXMoves} - */ - toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; - - /** - * Check whether a {@linkcode Pokemon}'s effective stat is as expected - * (checked after all stat value modifications). - * @param stat - The {@linkcode EffectiveStat} to check - * @param expectedValue - The expected value of {@linkcode stat} - * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher - * @remarks - * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. - */ - toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. - * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, - * or a partially filled {@linkcode Status} containing the desired properties - */ - toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. - * @param stat - The {@linkcode BattleStat} to check - * @param expectedStage - The expected stat stage value of {@linkcode stat} - */ - toHaveStatStage(stat: BattleStat, expectedStage: number): void; - - /** - * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. - * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties - */ - toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void; - /** - * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. - * @param expectedType - The expected {@linkcode BattlerTagType} - */ - toHaveBattlerTag(expectedType: BattlerTagType): void; - - /** - * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. - * @param expectedAbilityId - The `AbilityId` to check for - */ - toHaveAbilityApplied(expectedAbilityId: AbilityId): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. - * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have - */ - toHaveHp(expectedHp: number): void; - - /** - * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. - * @param expectedDamageTaken - The expected amount of damage taken - * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` - */ - toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; - - /** - * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). - * @remarks - * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. - * Otherwise, the Pokemon will be removed from the field and garbage collected. - */ - toHaveFainted(): void; - - /** - * Check whether a {@linkcode Pokemon} is at full HP. - */ - toHaveFullHp(): void; - /** - * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. - * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP - * @param ppUsed - The numerical amount of PP that should have been consumed, - * or `all` to indicate the move should be _out_ of PP - * @remarks - * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} - * or does not contain exactly one copy of `moveId`, this will fail the test. - */ - toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; - - // #endregion Pokemon Matchers - } + interface Assertion + extends GenericMatchers, + RestrictMatcher, + RestrictMatcher, + RestrictMatcher {} } + +/** + * Utility type to restrict matchers' properties based on the type of `T`. + * If it does not extend `R`, all methods inside `M` will have their types resolved to `never`. + * @typeParam M - The type of the matchers object to restrict + * @typeParam T - The type parameter of the assertion + * @typeParam R - The type to restrict T based off of + * @privateRemarks + * We cannot remove incompatible methods outright as Typescript requires that + * interfaces extend solely off of types with statically known members. + */ +type RestrictMatcher = { + [k in keyof M]: T extends R ? M[k] : never; +}; +// #endregion Boilerplate/Helpers + +// #region Generic Matchers +interface GenericMatchers { + /** + * Check whether an array contains EXACTLY the given items (in any order). + * + * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality + * (as opposed to full equality). + * + * @param expected - The expected contents of the array, in any order + * @see {@linkcode expect.arrayContaining} + */ + toEqualUnsorted: T extends (infer U)[] ? (expected: U[]) => void : never; + + /** + * Check whether a {@linkcode Map} contains the given key, disregarding its value. + * @param expectedKey - The key whose inclusion is being checked + * @privateRemarks + * While this functionality _could_ be simulated by writing + * `expect(x.get(y)).toBeDefined()` or + * `expect(x).toContain([y, expect.anything()])`, + * this is still preferred due to being more ergonomic and provides better error messsages. + */ + toHaveKey: T extends Map ? (expectedKey: K) => void : never; +} +// #endregion Generic Matchers + +// #region GameManager Matchers +interface GameManagerMatchers { + /** + * Check if the {@linkcode GameManager} has shown the given message at least once in the current test case. + * @param expectedMessage - The expected message to be displayed + * @remarks + * Strings consumed by this function should _always_ be produced by a call to `i18next.t` + * to avoid hardcoding text into test files. + */ + toHaveShownMessage(expectedMessage: string): void; + + /** + * Check if the currently-running {@linkcode Phase} is of the given type. + * @param expectedPhase - The expected {@linkcode PhaseString | name of the phase} + */ + toBeAtPhase(expectedPhase: PhaseString): void; +} // #endregion GameManager Matchers + +// #region Arena Matchers +interface ArenaMatchers { + /** + * Check whether the current {@linkcode WeatherType} is as expected. + * @param expectedWeatherType - The expected `WeatherType` + */ + toHaveWeather(expectedWeatherType: WeatherType): void; + + /** + * Check whether the current {@linkcode TerrainType} is as expected. + * @param expectedTerrainType - The expected `TerrainType` + */ + toHaveTerrain(expectedTerrainType: TerrainType): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedTag - A partially-filled `ArenaTag` containing the desired properties + */ + toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} + */ + toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. + * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties + */ + toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; + /** + * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. + * @param expectedType - The {@linkcode PositionalTagType} of the desired tag + * @param count - The number of instances of `expectedType` that should be active; + * defaults to `1` and must be within the range `[0, 4]` + */ + toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; +} + +// #endregion Arena Matchers + +// #region Pokemon Matchers +interface PokemonMatchers { + /** + * Check whether a {@linkcode Pokemon}'s current typing includes the given types. + * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` + * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher + */ + toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. + * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, + * or a partially filled {@linkcode TurnMove} containing the desired properties to check + * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` + * @see {@linkcode Pokemon.getLastXMoves} + */ + toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; + + /** + * Check whether a {@linkcode Pokemon}'s effective stat is as expected + * (checked after all stat value modifications). + * @param stat - The {@linkcode EffectiveStat} to check + * @param expectedValue - The expected value of {@linkcode stat} + * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher + * @remarks + * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. + */ + toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. + * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, + * or a partially filled {@linkcode Status} containing the desired properties + */ + toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. + * @param stat - The {@linkcode BattleStat} to check + * @param expectedStage - The expected stat stage value of {@linkcode stat} + */ + toHaveStatStage(stat: BattleStat, expectedStage: number): void; + + /** + * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. + * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties + */ + toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void; + /** + * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. + * @param expectedType - The expected {@linkcode BattlerTagType} + */ + toHaveBattlerTag(expectedType: BattlerTagType): void; + + /** + * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. + * @param expectedAbilityId - The `AbilityId` to check for + */ + toHaveAbilityApplied(expectedAbilityId: AbilityId): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. + * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have + */ + toHaveHp(expectedHp: number): void; + + /** + * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. + * @param expectedDamageTaken - The expected amount of damage taken + * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` + */ + toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; + + /** + * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). + * @remarks + * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. + * Otherwise, the Pokemon will be removed from the field and garbage collected. + */ + toHaveFainted(): void; + + /** + * Check whether a {@linkcode Pokemon} is at full HP. + */ + toHaveFullHp(): void; + + /** + * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. + * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP + * @param ppUsed - The numerical amount of PP that should have been consumed, + * or `all` to indicate the move should be _out_ of PP + * @remarks + * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} + * or does not contain exactly one copy of `moveId`, this will fail the test. + */ + toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; +} +// #endregion Pokemon Matchers diff --git a/test/abilities/supreme-overlord.test.ts b/test/abilities/supreme-overlord.test.ts index a0f2d9050b3..d5470b70476 100644 --- a/test/abilities/supreme-overlord.test.ts +++ b/test/abilities/supreme-overlord.test.ts @@ -1,6 +1,7 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import type { Move } from "#moves/move"; @@ -166,4 +167,41 @@ describe("Abilities - Supreme Overlord", () => { expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); }); + + it("should not increase in power if ally faints while on the field", async () => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + game.move.select(MoveId.TACKLE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(MoveId.LUNAR_DANCE, BattlerIndex.PLAYER_2); + await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + await game.toEndOfTurn(); + + expect(game.field.getPlayerPokemon()).not.toHaveBattlerTag(BattlerTagType.SUPREME_OVERLORD); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should persist fainted count through reload", async () => { + // Avoid learning moves + game.override.startingLevel(1000); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + game.move.select(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(MoveId.TACKLE); + await game.toEndOfTurn(); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1); + + await game.toNextWave(); + await game.reload.reloadSession(); + + expect(game.field.getPlayerPokemon()).toHaveBattlerTag({ tagType: BattlerTagType.SUPREME_OVERLORD, faintCount: 1 }); + + game.move.select(MoveId.TACKLE); + await game.toEndOfTurn(); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1); + }); }); diff --git a/test/moves/healing-wish-lunar-dance.test.ts b/test/moves/healing-wish-lunar-dance.test.ts new file mode 100644 index 00000000000..0dcf993aeac --- /dev/null +++ b/test/moves/healing-wish-lunar-dance.test.ts @@ -0,0 +1,245 @@ +import { AbilityId } from "#enums/ability-id"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Challenges } from "#enums/challenges"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Lunar Dance and Healing Wish", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override.battleStyle("double").enemyAbility(AbilityId.BALL_FETCH).enemyMoveset(MoveId.SPLASH); + }); + + describe.each([ + { moveName: "Healing Wish", moveId: MoveId.HEALING_WISH }, + { moveName: "Lunar Dance", moveId: MoveId.LUNAR_DANCE }, + ])("$moveName", ({ moveId }) => { + it("should sacrifice the user to restore the switched in Pokemon's HP", async () => { + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + squirtle.hp = 1; + + game.move.use(MoveId.SPLASH, 0); + game.move.use(moveId, 1); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + + expect(bulbasaur.isFullHp()).toBe(true); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.isFullHp()).toBe(true); + }); + + it("should sacrifice the user to cure the switched in Pokemon's status", async () => { + game.override.statusEffect(StatusEffect.BURN); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + + game.move.use(MoveId.SPLASH, 0); + game.move.use(moveId, 1); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + + expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.status?.effect).toBeUndefined(); + }); + + it("should fail if the user has no non-fainted allies in their party", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); + const [bulbasaur, charmander] = game.scene.getPlayerParty(); + + game.move.use(MoveId.MEMENTO); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isActive(true)).toBe(true); + + game.move.use(moveId); + + await game.toEndOfTurn(); + + expect(charmander.isFullHp()); + expect(charmander.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if the user has no challenge-eligible allies", async () => { + game.override.battleStyle("single"); + // Mono normal challenge + game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.NORMAL + 1, 0); + await game.challengeMode.startBattle([SpeciesId.RATICATE, SpeciesId.ODDISH]); + + const raticate = game.field.getPlayerPokemon(); + + game.move.use(moveId); + await game.toNextTurn(); + + expect(raticate.isFullHp()).toBe(true); + expect(raticate.getLastXMoves()[0].result).toEqual(MoveResult.FAIL); + }); + + it("should store its effect if the switched-in Pokemon would be unaffected", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + squirtle.hp = 1; + + game.move.use(moveId); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + + // Bulbasaur fainted and stored a healing effect + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + // Switch to damaged Squirtle. HW/LD's effect should activate + game.doSwitchPokemon(2); + + await game.toEndOfTurn(); + expect(squirtle.isFullHp()).toBe(true); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined(); + + // Set Charmander's HP to 1, then switch back to Charmander. + // HW/LD shouldn't activate again + charmander.hp = 1; + game.doSwitchPokemon(2); + + await game.toEndOfTurn(); + expect(charmander.hp).toBe(1); + }); + + it("should only store one charge of the effect at a time", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.PIKACHU, + ]); + + const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty(); + [squirtle, pikachu].forEach(p => (p.hp = 1)); + + // Use HW/LD and send in Charmander. HW/LD's effect should be stored + game.move.use(moveId); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(charmander.isFullHp()); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + // Use HW/LD again, sending in Squirtle. HW/LD should activate and heal Squirtle + game.move.use(moveId); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.isFullHp()).toBe(true); + expect(squirtle.isFullHp()); + + // Switch again to Pikachu. HW/LD's effect shouldn't be present + game.doSwitchPokemon(3); + + expect(pikachu.isFullHp()).toBe(false); + }); + }); + + it("Lunar Dance should sacrifice the user to restore the switched in Pokemon's PP", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); + + const [bulbasaur, charmander] = game.scene.getPlayerParty(); + + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.move.use(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(charmander.isFainted()).toBeTruthy(); + bulbasaur.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0)); + }); + + it("should stack with each other", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.PIKACHU, + ]); + + const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty(); + [squirtle, pikachu].forEach(p => { + p.hp = 1; + p.getMoveset().forEach(mv => (mv.ppUsed = 1)); + }); + + game.move.use(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + game.move.use(MoveId.HEALING_WISH); + game.doSelectPartyPokemon(2); + + // Lunar Dance should apply first since it was used first, restoring Squirtle's HP and PP + await game.toNextTurn(); + expect(squirtle.isFullHp()).toBe(true); + squirtle.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0)); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + game.doSwitchPokemon(3); + + // Healing Wish should apply on the next switch, restoring Pikachu's HP + await game.toEndOfTurn(); + expect(pikachu.isFullHp()).toBe(true); + pikachu.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(1)); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined(); + }); +}); diff --git a/test/moves/lunar-dance.test.ts b/test/moves/lunar-dance.test.ts deleted file mode 100644 index 7386d15079b..00000000000 --- a/test/moves/lunar-dance.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Moves - Lunar Dance", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .statusEffect(StatusEffect.BURN) - .battleStyle("double") - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should full restore HP, PP and status of switched in pokemon, then fail second use because no remaining backup pokemon in party", async () => { - await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.ODDISH, SpeciesId.RATTATA]); - - const [bulbasaur, oddish, rattata] = game.scene.getPlayerParty(); - game.move.changeMoveset(bulbasaur, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - game.move.changeMoveset(oddish, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - game.move.changeMoveset(rattata, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.SPLASH, 1); - await game.toNextTurn(); - - // Bulbasaur should still be burned and have used a PP for splash and not at max hp - expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); - expect(bulbasaur.moveset[1]?.ppUsed).toBe(1); - expect(bulbasaur.hp).toBeLessThan(bulbasaur.getMaxHp()); - - // Switch out Bulbasaur for Rattata so we can swtich bulbasaur back in with lunar dance - game.doSwitchPokemon(2); - game.move.select(MoveId.SPLASH, 1); - await game.toNextTurn(); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.LUNAR_DANCE); - game.doSelectPartyPokemon(2); - await game.phaseInterceptor.to("SwitchPhase", false); - await game.toNextTurn(); - - // Bulbasaur should NOT have any status and have full PP for splash and be at max hp - expect(bulbasaur.status?.effect).toBeUndefined(); - expect(bulbasaur.moveset[1]?.ppUsed).toBe(0); - expect(bulbasaur.isFullHp()).toBe(true); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.LUNAR_DANCE); - await game.toNextTurn(); - - // Using Lunar dance again should fail because nothing in party and rattata should be alive - expect(rattata.status?.effect).toBe(StatusEffect.BURN); - expect(rattata.hp).toBeLessThan(rattata.getMaxHp()); - }); -}); diff --git a/test/setup/matchers.setup.ts b/test/setup/matchers.setup.ts index 88ca0a5c6bc..8ad14c8679a 100644 --- a/test/setup/matchers.setup.ts +++ b/test/setup/matchers.setup.ts @@ -1,5 +1,11 @@ +/** + * Setup file for custom matchers. + * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too! + * @module + */ + import { toBeAtPhase } from "#test/test-utils/matchers/to-be-at-phase"; -import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted"; +import { toEqualUnsorted } from "#test/test-utils/matchers/to-equal-unsorted"; import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied"; import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag"; import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag"; @@ -7,6 +13,7 @@ import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted"; import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp"; import { toHaveHp } from "#test/test-utils/matchers/to-have-hp"; +import { toHaveKey } from "#test/test-utils/matchers/to-have-key"; import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag"; import { toHaveShownMessage } from "#test/test-utils/matchers/to-have-shown-message"; import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage"; @@ -19,13 +26,9 @@ import { toHaveUsedPP } from "#test/test-utils/matchers/to-have-used-pp"; import { toHaveWeather } from "#test/test-utils/matchers/to-have-weather"; import { expect } from "vitest"; -/* - * Setup file for custom matchers. - * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too! - */ - expect.extend({ - toEqualArrayUnsorted, + toEqualUnsorted, + toHaveKey, toHaveShownMessage, toBeAtPhase, toHaveWeather, diff --git a/test/test-utils/helpers/modifiers-helper.ts b/test/test-utils/helpers/modifiers-helper.ts index bfda35427fa..7d3e29c420f 100644 --- a/test/test-utils/helpers/modifiers-helper.ts +++ b/test/test-utils/helpers/modifiers-helper.ts @@ -40,10 +40,7 @@ export class ModifierHelper extends GameManagerHelper { * @returns `this` */ testCheck(modifier: ModifierTypeKeys, expectToBePreset: boolean): this { - if (expectToBePreset) { - expect(itemPoolChecks.get(modifier)).toBeTruthy(); - } - expect(itemPoolChecks.get(modifier)).toBeFalsy(); + (expectToBePreset ? expect(itemPoolChecks) : expect(itemPoolChecks).not).toHaveKey(modifier); return this; } diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-unsorted.ts similarity index 92% rename from test/test-utils/matchers/to-equal-array-unsorted.ts rename to test/test-utils/matchers/to-equal-unsorted.ts index 97398689032..c3d85288815 100644 --- a/test/test-utils/matchers/to-equal-array-unsorted.ts +++ b/test/test-utils/matchers/to-equal-unsorted.ts @@ -8,11 +8,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; * @param expected - The array to check equality with * @returns Whether the matcher passed */ -export function toEqualArrayUnsorted( - this: MatcherState, - received: unknown, - expected: unknown[], -): SyncExpectationResult { +export function toEqualUnsorted(this: MatcherState, received: unknown, expected: unknown[]): SyncExpectationResult { if (!Array.isArray(received)) { return { pass: this.isNot, diff --git a/test/test-utils/matchers/to-have-key.ts b/test/test-utils/matchers/to-have-key.ts new file mode 100644 index 00000000000..73d442fc979 --- /dev/null +++ b/test/test-utils/matchers/to-have-key.ts @@ -0,0 +1,47 @@ +import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { receivedStr } from "#test/test-utils/test-utils"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher that checks if a {@linkcode Map} contains the given key, regardless of its value. + * @param received - The received value. Should be a Map + * @param expectedKey - The key whose inclusion in the map is being checked + * @returns Whether the matcher passed + */ +export function toHaveKey(this: MatcherState, received: unknown, expectedKey: unknown): SyncExpectationResult { + if (!(received instanceof Map)) { + return { + pass: this.isNot, + message: () => `Expected to receive a Map, but got ${receivedStr(received)}!`, + }; + } + + if (received.size === 0) { + return { + pass: this.isNot, + message: () => "Expected to receive a non-empty Map, but received map was empty!", + expected: expectedKey, + actual: received, + }; + } + + const keys = [...received.keys()]; + const pass = this.equals(keys, expectedKey, [ + ...this.customTesters, + this.utils.iterableEquality, + this.utils.subsetEquality, + ]); + + const actualStr = getOnelineDiffStr.call(this, received); + const expectedStr = getOnelineDiffStr.call(this, expectedKey); + + return { + pass, + message: () => + pass + ? `Expected ${actualStr} to NOT have the key ${expectedStr}, but it did!` + : `Expected ${actualStr} to have the key ${expectedStr}, but it didn't!`, + expected: expectedKey, + actual: keys, + }; +} diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index f951abed0b3..9b6939168f0 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -8,8 +8,8 @@ import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils" import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher that checks if the {@linkcode TerrainType} is as expected - * @param received - The object to check. Should be an instance of {@linkcode GameManager}. + * Matcher that checks if the current {@linkcode TerrainType} is as expected. + * @param received - The object to check. Should be the current {@linkcode GameManager}. * @param expectedTerrainType - The expected {@linkcode TerrainType}, or {@linkcode TerrainType.NONE} if no terrain should be active * @returns Whether the matcher passed */ diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts index ffb1e0aad97..7604cd5f890 100644 --- a/test/test-utils/matchers/to-have-weather.ts +++ b/test/test-utils/matchers/to-have-weather.ts @@ -8,8 +8,8 @@ import { toTitleCase } from "#utils/strings"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher that checks if the {@linkcode WeatherType} is as expected - * @param received - The object to check. Expects an instance of {@linkcode GameManager}. + * Matcher that checks if the current {@linkcode WeatherType} is as expected. + * @param received - The object to check. Should be the current {@linkcode GameManager} * @param expectedWeatherType - The expected {@linkcode WeatherType} * @returns Whether the matcher passed */