diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 62351c4a833..92b2dfadea9 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -71,6 +71,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"; @@ -125,6 +126,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"; @@ -327,15 +329,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 85f3b963a93..59f73ccfc57 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -43,7 +43,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 { MovePPRestoredEvent } from "#events/battle-scene"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; import { BerryModifierType } from "#modifiers/modifier-type"; @@ -4750,8 +4749,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 MovePPRestoredEvent(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 f95eee9fd95..c08634301f6 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -6,7 +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 { MovePPRestoredEvent } from "#events/battle-scene"; +import { MovesetChangedEvent } from "#events/battle-scene"; import type { Pokemon } from "#field/pokemon"; import { NumberHolder, randSeedInt, toDmgValue } from "#utils/common"; import i18next from "i18next"; @@ -153,7 +153,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { berryName: getBerryName(berryType), }), ); - globalScene.eventTarget.dispatchEvent(new MovePPRestoredEvent(m)); + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(consumer.id, ppRestoreMove)); } } break; diff --git a/src/events/battle-scene.ts b/src/events/battle-scene.ts index 5945e8f849c..fdafb9a1d30 100644 --- a/src/events/battle-scene.ts +++ b/src/events/battle-scene.ts @@ -1,33 +1,42 @@ +// 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 whenever a Pokemon's moveset is changed or altered - whether from moveset-overridding effects, + * Emitted whenever a Pokemon's moveset is changed or altered - whether from moveset-overridding effects, * PP consumption or restoration. * @see {@linkcode MovesetChangedEvent} */ - MOVESET_CHANGED = "onMovePPChanged", + 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 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", @@ -42,9 +51,12 @@ abstract class BattleSceneEvent extends Event { constructor(type: BattleSceneEventType) { super(type); } -} /** +} + +export type { BattleSceneEvent }; + +/** * Container class for {@linkcode BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED} events - * @extends Event */ export class CandyUpgradeNotificationChangedEvent extends BattleSceneEvent { declare type: BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED; @@ -59,7 +71,7 @@ export class CandyUpgradeNotificationChangedEvent extends BattleSceneEvent { /** * Container class for {@linkcode BattleSceneEventType.MOVESET_CHANGED} events. \ - * Emitted whenever the moveset of any on-field Pokemon is changed, or a move's PP is increased or decreased. + * Emitted whenever the moveset of any {@linkcode Pokemon} is changed, or a move's PP is increased or decreased. */ export class MovesetChangedEvent extends BattleSceneEvent { declare type: BattleSceneEventType.MOVESET_CHANGED; @@ -80,6 +92,24 @@ export class MovesetChangedEvent extends BattleSceneEvent { } } +/** + * 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 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. */ @@ -106,9 +136,9 @@ export class TurnEndEvent extends BattleSceneEvent { } /** * 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 32edd721cd9..5fc23ae51fd 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -108,6 +108,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 { SummonDataResetEvent } from "#events/battle-scene"; import { doShinySparkleAnim } from "#field/anims"; import { BaseStatModifier, @@ -1828,7 +1829,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[] { @@ -5084,6 +5085,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/move-phase.ts b/src/phases/move-phase.ts index 585e2455589..c49a5a8ed17 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -334,7 +334,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 MovesetChangedEvent(this.pokemon.id, move.id, this.move.ppUsed)); + globalScene.eventTarget.dispatchEvent(new MovesetChangedEvent(this.pokemon.id, this.move)); } /** @@ -637,7 +637,7 @@ 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 MovesetChangedEvent(this.pokemon.id, this.move)); 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/ui/battle-flyout.ts b/src/ui/battle-flyout.ts index 584ac12528e..f7442b4de55 100644 --- a/src/ui/battle-flyout.ts +++ b/src/ui/battle-flyout.ts @@ -2,7 +2,7 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { MoveId } from "#enums/move-id"; import { UiTheme } from "#enums/ui-theme"; -import type { MovesetChangedEvent } from "#events/battle-scene"; +import type { MovesetChangedEvent, SummonDataResetEvent } from "#events/battle-scene"; import { BattleSceneEventType } from "#events/battle-scene"; import type { Pokemon } from "#field/pokemon"; import type { PokemonMove } from "#moves/pokemon-move"; @@ -11,7 +11,7 @@ import type { BattleInfo } from "#ui/battle-info"; import { addTextObject, TextStyle } from "#ui/text"; import { fixedInt } from "#utils/common"; -/** Container for info about the {@linkcode PokemonMove}s having been */ +/** Container for info about a given {@linkcode PokemonMove} having been used */ interface MoveInfo { /** The name of the {@linkcode Move} having been used. */ name: string; @@ -19,14 +19,22 @@ interface MoveInfo { move: PokemonMove; } +/** + * 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 {@linkcode BattleInfo} object on the field UI */ +/** + * 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 */ @@ -53,12 +61,20 @@ export class BattleFlyout extends Phaser.GameObjects.Container { private flyoutText: Phaser.GameObjects.Text[] = new Array(4); /** 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 onMovesetChangedEvent = (event: MovesetChangedEvent) => this.onMovesetChanged(event); + private readonly onMovesetRestoredEvent = (event: SummonDataResetEvent) => this.onMovesetRestored(event); constructor(player: boolean) { super(globalScene, 0, 0); @@ -138,13 +154,14 @@ export class BattleFlyout extends Phaser.GameObjects.Container { * 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) { - const flyoutText = this.flyoutText[index]; - const moveInfo = this.moveInfo[index]; + 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 flyoutText = this.flyoutText[index]; const maxPP = moveInfo.move.getMovePp(); const currentPp = -moveInfo.move.ppUsed; flyoutText.text = `${moveInfo.name} ${currentPp}/${maxPP}`; @@ -164,15 +181,20 @@ export class BattleFlyout extends Phaser.GameObjects.Container { return; } - // If we already have a move in that slot, update the corresponding slot of the Pokemon's moveset. - const index = this.pokemon.getMoveset().indexOf(event.move); + // 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; + + const index = this.pokemon.getMoveset(isPermanent).indexOf(event.move); if (index === -1) { - console.error("Updated move passed to move flyout was undefined!"); + throw new Error("Updated move passed to move flyout was not found in moveset!"); } - if (this.moveInfo[index]) { - this.moveInfo[index].move = event.move; + + // 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 { - this.moveInfo[index] = { + infoArray[index] = { name: event.move.getMove().name, move: event.move, }; @@ -181,7 +203,23 @@ export class BattleFlyout extends Phaser.GameObjects.Container { this.updateText(index); } - /** Animates the flyout to either show or hide it by applying a fade and translation */ + /** + * Reset the linked Pokemon's temporary moveset override when it is switched out. + * @param event - The {@linkcode SummonDataResetEvent} having been emitted + */ + private onMovesetRestored(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;