diff --git a/src/@types/typed-event-target.ts b/src/@types/typed-event-target.ts new file mode 100644 index 00000000000..2c38a6812d6 --- /dev/null +++ b/src/@types/typed-event-target.ts @@ -0,0 +1,17 @@ +/** + * Interface restricting the events emitted by an {@linkcode EventTarget} to a certain kind of {@linkcode Event}. + * @typeParam T - The type to restrict the interface's access; must extend from {@linkcode Event} + */ +export interface TypedEventTarget extends EventTarget { + dispatchEvent(event: T): boolean; + addEventListener( + event: T["type"], + callback: EventListenerOrEventListenerObject | null, + options?: AddEventListenerOptions | boolean, + ): void; + removeEventListener( + type: T["type"], + callback: EventListenerOrEventListenerObject | null, + options?: EventListenerOptions | boolean, + ): void; +} diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 8c8906be2b0..10b92ec08de 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -72,6 +72,7 @@ import type { TrainerSlot } from "#enums/trainer-slot"; import { TrainerType } from "#enums/trainer-type"; import { TrainerVariant } from "#enums/trainer-variant"; import { UiTheme } from "#enums/ui-theme"; +import type { BattleSceneEvent } from "#events/battle-scene"; import { NewArenaEvent } from "#events/battle-scene"; import { Arena, ArenaBase } from "#field/arena"; import { DamageNumberHandler } from "#field/damage-number-handler"; @@ -126,6 +127,7 @@ import { vouchers } from "#system/voucher"; import { trainerConfigs } from "#trainers/trainer-config"; import type { HeldModifierConfig } from "#types/held-modifier-config"; import type { Localizable } from "#types/locales"; +import type { TypedEventTarget } from "#types/typed-event-target"; import { AbilityBar } from "#ui/ability-bar"; import { ArenaFlyout } from "#ui/arena-flyout"; import { CandyBar } from "#ui/candy-bar"; @@ -329,15 +331,9 @@ export class BattleScene extends SceneBase { public eventManager: TimedEventManager; /** - * Allows subscribers to listen for events - * - * Current Events: - * - {@linkcode BattleSceneEventType.MOVE_USED} {@linkcode MoveUsedEvent} - * - {@linkcode BattleSceneEventType.TURN_INIT} {@linkcode TurnInitEvent} - * - {@linkcode BattleSceneEventType.TURN_END} {@linkcode TurnEndEvent} - * - {@linkcode BattleSceneEventType.NEW_ARENA} {@linkcode NewArenaEvent} + * Allows subscribers to listen for events. */ - public readonly eventTarget: EventTarget = new EventTarget(); + public readonly eventTarget: TypedEventTarget = new EventTarget(); constructor() { super("battle"); diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 0ee1a51a78e..a6d39e482bf 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -44,7 +44,6 @@ import { BATTLE_STATS, EFFECTIVE_STATS, getStatKey, Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { SwitchType } from "#enums/switch-type"; import { WeatherType } from "#enums/weather-type"; -import { BerryUsedEvent } from "#events/battle-scene"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; import { BerryModifierType } from "#modifiers/modifier-type"; @@ -4751,8 +4750,6 @@ export class CudChewConsumeBerryAbAttr extends AbAttr { // This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed. for (const berryType of pokemon.summonData.berriesEatenLast) { getBerryEffectFunc(berryType)(pokemon); - const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1); - globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message } // uncomment to make cheek pouch work with cud chew diff --git a/src/data/berry.ts b/src/data/berry.ts index 61235b75e21..c08634301f6 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -6,6 +6,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { HitResult } from "#enums/hit-result"; import { type BattleStat, Stat } from "#enums/stat"; +import { MovesetChangedEvent } from "#events/battle-scene"; import type { Pokemon } from "#field/pokemon"; import { NumberHolder, randSeedInt, toDmgValue } from "#utils/common"; import i18next from "i18next"; @@ -152,6 +153,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { berryName: getBerryName(berryType), }), ); + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(consumer.id, ppRestoreMove)); } } break; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index bde5f2977d8..34a209c11b1 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -67,7 +67,7 @@ import { import { StatusEffect } from "#enums/status-effect"; import { SwitchType } from "#enums/switch-type"; import { WeatherType } from "#enums/weather-type"; -import { MoveUsedEvent } from "#events/battle-scene"; +import { MovesetChangedEvent } from "#events/battle-scene"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import { AttackTypeBoosterModifier, @@ -7314,8 +7314,8 @@ export class ReducePpMoveAttr extends MoveEffectAttr { const lastPpUsed = movesetMove.ppUsed; movesetMove.ppUsed = Math.min(lastPpUsed + this.reduction, movesetMove.getMovePp()); - globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(target.id, movesetMove.getMove(), movesetMove.ppUsed)); globalScene.phaseManager.queueMessage(i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed })); + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(target.id, movesetMove)); return true; } @@ -7419,11 +7419,13 @@ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr { return false; } - // Populate summon data with a copy of the current moveset, replacing the copying move with the copied move + // Populate summon data with a copy of the current moveset, replacing the copying move with the copied move. user.summonData.moveset = user.getMoveset().slice(0); - user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id); + const newMove = new PokemonMove(copiedMove.id); + user.summonData.moveset[thisMoveIndex] = newMove; globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedMove", { pokemonName: getPokemonNameWithAffix(user), moveName: copiedMove.name })); + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(user.id, newMove)); return true; } diff --git a/src/events/battle-scene.ts b/src/events/battle-scene.ts index 29aee1053cd..fdafb9a1d30 100644 --- a/src/events/battle-scene.ts +++ b/src/events/battle-scene.ts @@ -1,53 +1,65 @@ -import type { BerryModifier } from "#modifiers/modifier"; -import type { Move } from "#moves/move"; +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { PokemonSummonData } from "#data/pokemon-data"; +import type { PokemonMove } from "#moves/pokemon-move"; -/** Alias for all {@linkcode BattleScene} events */ +/** Enum comprising all {@linkcode BattleScene} events that can be emitted. */ export enum BattleSceneEventType { /** - * Triggers when the corresponding setting is changed + * Emitted when the corresponding setting is changed * @see {@linkcode CandyUpgradeNotificationChangedEvent} */ CANDY_UPGRADE_NOTIFICATION_CHANGED = "onCandyUpgradeNotificationChanged", /** - * Triggers when a move is successfully used - * @see {@linkcode MoveUsedEvent} + * Emitted whenever a Pokemon's moveset is changed or altered - whether from moveset-overridding effects, + * PP consumption or restoration. + * @see {@linkcode MovesetChangedEvent} */ - MOVE_USED = "onMoveUsed", - /** - * Triggers when a berry gets successfully used - * @see {@linkcode BerryUsedEvent} - */ - BERRY_USED = "onBerryUsed", + MOVESET_CHANGED = "onMovesetChanged", /** - * Triggers at the start of each new encounter + * Emitted whenever the {@linkcode PokemonSummonData} of any {@linkcode Pokemon} is reset to its initial state + * (such as immediately before a switch-out). + * @see {@linkcode SummonDataResetEvent} + */ + SUMMON_DATA_RESET = "onSummonDataReset", + + /** + * Emitted at the start of each new encounter * @see {@linkcode EncounterPhaseEvent} */ ENCOUNTER_PHASE = "onEncounterPhase", /** - * Triggers on the first turn of a new battle - * @see {@linkcode TurnInitEvent} - */ - TURN_INIT = "onTurnInit", - /** - * Triggers after a turn ends in battle + * Emitted after a turn ends in battle * @see {@linkcode TurnEndEvent} */ TURN_END = "onTurnEnd", /** - * Triggers when a new {@linkcode Arena} is created during initialization + * Emitted when a new {@linkcode Arena} is created during initialization * @see {@linkcode NewArenaEvent} */ NEW_ARENA = "onNewArena", } /** - * Container class for {@linkcode BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED} events - * @extends Event + * Abstract container class for all {@linkcode BattleSceneEventType} events. */ -export class CandyUpgradeNotificationChangedEvent extends Event { +abstract class BattleSceneEvent extends Event { + public declare abstract readonly type: BattleSceneEventType; // that's a mouthful! + // biome-ignore lint/complexity/noUselessConstructor: changes the type of the type field + constructor(type: BattleSceneEventType) { + super(type); + } +} + +export type { BattleSceneEvent }; + +/** + * Container class for {@linkcode BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED} events + */ +export class CandyUpgradeNotificationChangedEvent extends BattleSceneEvent { + declare type: BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED; /** The new value the setting was changed to */ public newValue: number; constructor(newValue: number) { @@ -58,61 +70,62 @@ export class CandyUpgradeNotificationChangedEvent extends Event { } /** - * Container class for {@linkcode BattleSceneEventType.MOVE_USED} events - * @extends Event + * Container class for {@linkcode BattleSceneEventType.MOVESET_CHANGED} events. \ + * Emitted whenever the moveset of any {@linkcode Pokemon} is changed, or a move's PP is increased or decreased. */ -export class MoveUsedEvent extends Event { - /** The ID of the {@linkcode Pokemon} that used the {@linkcode Move} */ +export class MovesetChangedEvent extends BattleSceneEvent { + declare type: BattleSceneEventType.MOVESET_CHANGED; + + /** The {@linkcode Pokemon.ID | ID} of the {@linkcode Pokemon} whose moveset has changed. */ public pokemonId: number; - /** The {@linkcode Move} used */ - public move: Move; - /** The amount of PP used on the {@linkcode Move} this turn */ - public ppUsed: number; - constructor(userId: number, move: Move, ppUsed: number) { - super(BattleSceneEventType.MOVE_USED); + /** + * The {@linkcode PokemonMove} having been changed. + * Will override the corresponding slot of the moveset flyout for that Pokemon. + */ + public move: PokemonMove; - this.pokemonId = userId; + constructor(pokemonId: number, move: PokemonMove) { + super(BattleSceneEventType.MOVESET_CHANGED); + + this.pokemonId = pokemonId; this.move = move; - this.ppUsed = ppUsed; - } -} -/** - * Container class for {@linkcode BattleSceneEventType.BERRY_USED} events - * @extends Event - */ -export class BerryUsedEvent extends Event { - /** The {@linkcode BerryModifier} being used */ - public berryModifier: BerryModifier; - constructor(berry: BerryModifier) { - super(BattleSceneEventType.BERRY_USED); - - this.berryModifier = berry; } } /** - * Container class for {@linkcode BattleSceneEventType.ENCOUNTER_PHASE} events - * @extends Event + * Container class for {@linkcode BattleSceneEventType.SUMMON_DATA_RESET} events. \ + * Emitted whenever the {@linkcode PokemonSummonData} of any {@linkcode Pokemon} is reset to its initial state + * (such as immediately before a switch-out). */ -export class EncounterPhaseEvent extends Event { +export class SummonDataResetEvent extends BattleSceneEvent { + declare type: BattleSceneEventType.SUMMON_DATA_RESET; + + /** The {@linkcode Pokemon.ID | ID} of the {@linkcode Pokemon} whose data has been reset. */ + public pokemonId: number; + + constructor(pokemonId: number) { + super(BattleSceneEventType.SUMMON_DATA_RESET); + + this.pokemonId = pokemonId; + } +} + +/** + * Container class for {@linkcode BattleSceneEventType.ENCOUNTER_PHASE} events. + */ +export class EncounterPhaseEvent extends BattleSceneEvent { + declare type: BattleSceneEventType.ENCOUNTER_PHASE; constructor() { super(BattleSceneEventType.ENCOUNTER_PHASE); } } -/** - * Container class for {@linkcode BattleSceneEventType.TURN_INIT} events - * @extends Event - */ -export class TurnInitEvent extends Event { - constructor() { - super(BattleSceneEventType.TURN_INIT); - } -} + /** * Container class for {@linkcode BattleSceneEventType.TURN_END} events * @extends Event */ -export class TurnEndEvent extends Event { +export class TurnEndEvent extends BattleSceneEvent { + declare type: BattleSceneEventType.TURN_END; /** The amount of turns in the current battle */ public turnCount: number; constructor(turnCount: number) { @@ -123,9 +136,9 @@ export class TurnEndEvent extends Event { } /** * Container class for {@linkcode BattleSceneEventType.NEW_ARENA} events - * @extends Event */ -export class NewArenaEvent extends Event { +export class NewArenaEvent extends BattleSceneEvent { + declare type: BattleSceneEventType.NEW_ARENA; constructor() { super(BattleSceneEventType.NEW_ARENA); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0523671ee5f..6e10d9fead3 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -107,6 +107,7 @@ import { SwitchType } from "#enums/switch-type"; import type { TrainerSlot } from "#enums/trainer-slot"; import { UiMode } from "#enums/ui-mode"; import { WeatherType } from "#enums/weather-type"; +import { MovesetChangedEvent, SummonDataResetEvent } from "#events/battle-scene"; import { doShinySparkleAnim } from "#field/anims"; import { BaseStatModifier, @@ -1817,7 +1818,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return all the {@linkcode PokemonMove}s that make up this Pokemon's moveset. * Takes into account player/enemy moveset overrides (which will also override PP count). - * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false` + * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode MoveId.TRANSFORM | Transform}; default `false` * @returns An array of {@linkcode PokemonMove}, as described above. */ getMoveset(ignoreOverride = false): PokemonMove[] { @@ -2822,6 +2823,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (this.summonData.moveset) { this.summonData.moveset[moveIndex] = move; } + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(this.id, move)); } /** @@ -5071,6 +5073,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.summonData.speciesForm = null; this.updateFusionPalette(); } + + // Emit an event to reset all temporary moveset overrides due to Mimic/Transform wearing off. + globalScene.eventTarget.dispatchEvent(new SummonDataResetEvent(this.id)); + this.summonData = new PokemonSummonData(); this.tempSummonData = new PokemonTempSummonData(); this.summonData.illusion = illusion; diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index 941406d0b96..77c2c4b3b58 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -2,7 +2,6 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { CommonAnim } from "#enums/move-anims-common"; -import { BerryUsedEvent } from "#events/battle-scene"; import type { Pokemon } from "#field/pokemon"; import { BerryModifier } from "#modifiers/modifier"; import { FieldPhase } from "#phases/field-phase"; @@ -65,7 +64,6 @@ export class BerryPhase extends FieldPhase { berryModifier.consumed = false; pokemon.loseHeldItem(berryModifier); } - globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); } globalScene.updateModifiers(pokemon.isPlayer()); diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index cd7c7a8f48f..a9e97953346 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -18,7 +18,7 @@ import { MoveResult } from "#enums/move-result"; import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { PokemonType } from "#enums/pokemon-type"; import { StatusEffect } from "#enums/status-effect"; -import { MoveUsedEvent } from "#events/battle-scene"; +import { MovesetChangedEvent } from "#events/battle-scene"; import type { Pokemon } from "#field/pokemon"; import { applyMoveAttrs } from "#moves/apply-attrs"; import { frenzyMissFunc } from "#moves/move-utils"; @@ -317,7 +317,7 @@ export class MovePhase extends BattlePhase { // "commit" to using the move, deducting PP. const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); this.move.usePp(ppUsed); - globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon.id, move, this.move.ppUsed)); + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(this.pokemon.id, this.move)); } /** @@ -620,10 +620,10 @@ export class MovePhase extends BattlePhase { } if (this.failed) { - // TODO: should this consider struggle? + // TODO: should this consider struggle and pressure? const ppUsed = isIgnorePP(this.useMode) ? 0 : 1; this.move.usePp(ppUsed); - globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(this.pokemon.id, this.move)); } if (this.cancelled && this.pokemon.summonData.tags.some(t => t.tagType === BattlerTagType.FRENZY)) { diff --git a/src/phases/pokemon-transform-phase.ts b/src/phases/pokemon-transform-phase.ts index 143c47886b6..9451d5bffdb 100644 --- a/src/phases/pokemon-transform-phase.ts +++ b/src/phases/pokemon-transform-phase.ts @@ -4,6 +4,7 @@ import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; +import { MovesetChangedEvent } from "#events/battle-scene"; import { PokemonMove } from "#moves/pokemon-move"; import { PokemonPhase } from "#phases/pokemon-phase"; import i18next from "i18next"; @@ -50,12 +51,14 @@ export class PokemonTransformPhase extends PokemonPhase { user.setStatStage(s, target.getStatStage(s)); } - user.summonData.moveset = target.getMoveset().map(m => { - if (m) { + user.summonData.moveset = target.getMoveset().map(oldMove => { + if (oldMove) { // If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5. - return new PokemonMove(m.moveId, 0, 0, Math.min(m.getMove().pp, 5)); + const newMove = new PokemonMove(oldMove.moveId, 0, 0, Math.min(oldMove.getMove().pp, 5)); + this.emitMovesetChange(oldMove, newMove); + return newMove; } - console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`); + console.warn(`Transform: somehow iterating over a ${oldMove} value when copying moveset!`); return new PokemonMove(MoveId.NONE); }); @@ -86,4 +89,21 @@ export class PokemonTransformPhase extends PokemonPhase { Promise.allSettled(promises).then(() => this.end()); } + + /** + * Emit an event upon transforming and changing movesets. + * @param origMovee - The target's original {@linkcode PokemonMove} being copied + * @param copiedMove - The new {@linkcode PokemonMove} being added to the user's moveset + */ + private emitMovesetChange(origMove: PokemonMove, copiedMove: PokemonMove): void { + const user = this.getPokemon(); + const target = globalScene.getField()[this.targetIndex]; + + // Dispatch an event for the user's moveset temporarily changing. + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(user.id, copiedMove)); + // If the user is a player having transformed into an enemy, permanently reveal the corresponding move in their moveset. + if (user.isPlayer() && target.isEnemy()) { + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(target.id, origMove)); + } + } } diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index b2ab096102c..aacfbed4435 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -1,6 +1,5 @@ import { globalScene } from "#app/global-scene"; import { BattlerIndex } from "#enums/battler-index"; -import { TurnInitEvent } from "#events/battle-scene"; import type { PlayerPokemon } from "#field/pokemon"; import { handleMysteryEncounterBattleStartEffects, @@ -46,8 +45,6 @@ export class TurnInitPhase extends FieldPhase { } }); - globalScene.eventTarget.dispatchEvent(new TurnInitEvent()); - handleMysteryEncounterBattleStartEffects(); // If true, will skip remainder of current phase (and not queue CommandPhases etc.) diff --git a/src/ui/battle-flyout.ts b/src/ui/battle-flyout.ts index 0a67dc9ad37..5625e4c7dd9 100644 --- a/src/ui/battle-flyout.ts +++ b/src/ui/battle-flyout.ts @@ -1,33 +1,41 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import { BerryType } from "#enums/berry-type"; import { MoveId } from "#enums/move-id"; import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; -import type { BerryUsedEvent, MoveUsedEvent } from "#events/battle-scene"; +import type { MovesetChangedEvent, SummonDataResetEvent } from "#events/battle-scene"; import { BattleSceneEventType } from "#events/battle-scene"; -import type { EnemyPokemon, Pokemon } from "#field/pokemon"; -import type { Move } from "#moves/move"; +import type { Pokemon } from "#field/pokemon"; +import type { PokemonMove } from "#moves/pokemon-move"; +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { BattleInfo } from "#ui/battle-info"; import { addTextObject } from "#ui/text"; import { fixedInt } from "#utils/common"; -/** Container for info about a {@linkcode Move} */ +/** Container for info about a given {@linkcode PokemonMove} having been used */ interface MoveInfo { - /** The {@linkcode Move} itself */ - move: Move; - - /** The maximum PP of the {@linkcode Move} */ - maxPp: number; - /** The amount of PP used by the {@linkcode Move} */ - ppUsed: number; + /** The name of the {@linkcode Move} having been used. */ + name: string; + /** The {@linkcode PokemonMove} having been used. */ + move: PokemonMove; } -/** A Flyout Menu attached to each {@linkcode BattleInfo} object on the field UI */ +/** + * A 4-length tuple consisting of all moves that each {@linkcode Pokemon} has used in the given battle. + * Entries that are `undefined` indicate moves which have not been used yet. + */ +type MoveInfoTuple = [MoveInfo?, MoveInfo?, MoveInfo?, MoveInfo?]; + +/** + * A Flyout Menu attached to each Pokemon's {@linkcode BattleInfo} object, + * showing all revealed moves and their current PP counts. + * @todo Stop tracking player move usages + */ export class BattleFlyout extends Phaser.GameObjects.Container { /** Is this object linked to a player's Pokemon? */ private player: boolean; - /** The Pokemon this object is linked to */ + /** The Pokemon this object is linked to. */ private pokemon: Pokemon; /** The restricted width of the flyout which should be drawn to */ @@ -52,15 +60,22 @@ export class BattleFlyout extends Phaser.GameObjects.Container { /** The array of {@linkcode Phaser.GameObjects.Text} objects which are drawn on the flyout */ private flyoutText: Phaser.GameObjects.Text[] = new Array(4); - /** The array of {@linkcode MoveInfo} used to track moves for the {@linkcode Pokemon} linked to the flyout */ - private moveInfo: MoveInfo[] = []; + /** An array of {@linkcode MoveInfo}s used to track moves for the {@linkcode Pokemon} linked to the flyout. */ + private moveInfo: MoveInfoTuple = []; + /** + * An array of {@linkcode MoveInfo}s used to track move slots + * temporarily overridden by {@linkcode MoveId.TRANSFORM} or {@linkcode MoveId.MIMIC}. + * + * Reset once {@linkcode pokemon} switches out via a {@linkcode SummonDataResetEvent}. + */ + private tempMoveInfo: MoveInfoTuple = []; /** Current state of the flyout's visibility */ public flyoutVisible = false; // Stores callbacks in a variable so they can be unsubscribed from when destroyed - private readonly onMoveUsedEvent = (event: Event) => this.onMoveUsed(event); - private readonly onBerryUsedEvent = (event: Event) => this.onBerryUsed(event); + private readonly onMovesetChangedEvent = (event: MovesetChangedEvent) => this.onMovesetChanged(event); + private readonly onSummonDataResetEvent = (event: SummonDataResetEvent) => this.onSummonDataReset(event); constructor(player: boolean) { super(globalScene, 0, 0); @@ -124,79 +139,90 @@ export class BattleFlyout extends Phaser.GameObjects.Container { } /** - * Links the given {@linkcode Pokemon} and subscribes to the {@linkcode BattleSceneEventType.MOVE_USED} event - * @param pokemon {@linkcode Pokemon} to link to this flyout + * Link the given {@linkcode Pokemon} to this flyout and subscribe to the {@linkcode BattleSceneEventType.MOVESET_CHANGED} event. + * @param pokemon - The {@linkcode Pokemon} to link to this flyout */ - initInfo(pokemon: EnemyPokemon) { + public initInfo(pokemon: Pokemon): void { this.pokemon = pokemon; this.name = `Flyout ${getPokemonNameWithAffix(this.pokemon)}`; this.flyoutParent.name = `Flyout Parent ${getPokemonNameWithAffix(this.pokemon)}`; - globalScene.eventTarget.addEventListener(BattleSceneEventType.MOVE_USED, this.onMoveUsedEvent); - globalScene.eventTarget.addEventListener(BattleSceneEventType.BERRY_USED, this.onBerryUsedEvent); + globalScene.eventTarget.addEventListener(BattleSceneEventType.MOVESET_CHANGED, this.onMovesetChangedEvent); + globalScene.eventTarget.addEventListener(BattleSceneEventType.SUMMON_DATA_RESET, this.onSummonDataResetEvent); } - /** Sets and formats the text property for all {@linkcode Phaser.GameObjects.Text} in the flyoutText array */ - private setText() { - for (let i = 0; i < this.flyoutText.length; i++) { - const flyoutText = this.flyoutText[i]; - const moveInfo = this.moveInfo[i]; - - if (!moveInfo) { - continue; - } - - const currentPp = moveInfo.maxPp - moveInfo.ppUsed; - flyoutText.text = `${moveInfo.move.name} ${currentPp}/${moveInfo.maxPp}`; - } - } - - /** Updates all of the {@linkcode MoveInfo} objects in the moveInfo array */ - private onMoveUsed(event: Event) { - const moveUsedEvent = event as MoveUsedEvent; - if (!moveUsedEvent || moveUsedEvent.pokemonId !== this.pokemon?.id || moveUsedEvent.move.id === MoveId.STRUGGLE) { - // Ignore Struggle + /** + * Set and formats the text property for all {@linkcode Phaser.GameObjects.Text} in the flyoutText array. + * @param index - The 0-indexed position of the flyout text object to update + */ + private updateText(index: number): void { + // Use temp move info if present, or else the regular move info. + const moveInfo = this.tempMoveInfo[index] ?? this.moveInfo[index]; + if (!moveInfo) { return; } - const foundInfo = this.moveInfo.find(x => x?.move.id === moveUsedEvent.move.id); - if (foundInfo) { - foundInfo.ppUsed = moveUsedEvent.ppUsed; - } else { - this.moveInfo.push({ - move: moveUsedEvent.move, - maxPp: moveUsedEvent.move.pp, - ppUsed: moveUsedEvent.ppUsed, - }); - } - - this.setText(); + const flyoutText = this.flyoutText[index]; + const maxPP = moveInfo.move.getMovePp(); + const currentPp = -moveInfo.move.ppUsed; + flyoutText.text = `${moveInfo.name} ${currentPp}/${maxPP}`; } - private onBerryUsed(event: Event) { - const berryUsedEvent = event as BerryUsedEvent; + /** + * Update the corresponding {@linkcode MoveInfo} object in the moveInfo array. + * @param event - The {@linkcode MovesetChangedEvent} having been emitted + */ + private onMovesetChanged(event: MovesetChangedEvent): void { + // Ignore other Pokemon's moves as well as Struggle and MoveId.NONE if ( - !berryUsedEvent || - berryUsedEvent.berryModifier.pokemonId !== this.pokemon?.id || - berryUsedEvent.berryModifier.berryType !== BerryType.LEPPA + event.pokemonId !== this.pokemon.id || + event.move.moveId === MoveId.NONE || + event.move.moveId === MoveId.STRUGGLE ) { - // We only care about Leppa berries return; } - const foundInfo = this.moveInfo.find(info => info.ppUsed === info.maxPp); - if (!foundInfo) { - // This will only happen on a de-sync of PP tracking - return; - } - foundInfo.ppUsed = Math.max(foundInfo.ppUsed - 10, 0); + // Push to either the temporary or permanent move arrays, depending on which array the move was found in. + const isPermanent = this.pokemon.getMoveset(true).includes(event.move); + const infoArray = isPermanent ? this.moveInfo : this.tempMoveInfo; - this.setText(); + const index = this.pokemon.getMoveset(isPermanent).indexOf(event.move); + if (index === -1) { + throw new Error("Updated move passed to move flyout was not found in moveset!"); + } + + // Update the corresponding slot in the info array with either a new entry or an updated PP reading. + if (infoArray[index]) { + infoArray[index].move = event.move; + } else { + infoArray[index] = { + name: event.move.getMove().name, + move: event.move, + }; + } + + this.updateText(index); } - /** Animates the flyout to either show or hide it by applying a fade and translation */ - toggleFlyout(visible: boolean): void { + /** + * Reset the linked Pokemon's temporary moveset override when it is switched out. + * @param event - The {@linkcode SummonDataResetEvent} having been emitted + */ + private onSummonDataReset(event: SummonDataResetEvent): void { + if (event.pokemonId !== this.pokemon.id) { + // Wrong pokemon + return; + } + + this.tempMoveInfo = []; + } + + /** + * Animate the flyout to either show or hide the modal. + * @param visible - Whether the the flyout should be shown + */ + public toggleFlyout(visible: boolean): void { this.flyoutVisible = visible; globalScene.tweens.add({ @@ -208,9 +234,10 @@ export class BattleFlyout extends Phaser.GameObjects.Container { }); } - destroy(fromScene?: boolean): void { - globalScene.eventTarget.removeEventListener(BattleSceneEventType.MOVE_USED, this.onMoveUsedEvent); - globalScene.eventTarget.removeEventListener(BattleSceneEventType.BERRY_USED, this.onBerryUsedEvent); + /** Destroy this element and remove all associated listeners. */ + public destroy(fromScene?: boolean): void { + globalScene.eventTarget.removeEventListener(BattleSceneEventType.MOVESET_CHANGED, this.onMovesetChangedEvent); + globalScene.eventTarget.removeEventListener(BattleSceneEventType.SUMMON_DATA_RESET, this.onSummonDataResetEvent); super.destroy(fromScene); }