diff --git a/src/battle-scene.ts b/src/battle-scene.ts index b802466ee19..9491753f6a3 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -894,9 +894,19 @@ export default class BattleScene extends SceneBase { return activeOnly ? this.infoToggles.filter(t => t?.isActive()) : this.infoToggles; } - getPokemonById(pokemonId: number): Pokemon | null { - const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId); - return (findInParty(this.getPlayerParty()) || findInParty(this.getEnemyParty())) ?? null; + /** + * Return the {@linkcode Pokemon} associated with a given ID. + * @param pokemonId - The ID whose Pokemon will be retrieved. + * @returns The {@linkcode Pokemon} associated with the given id. + * Returns `null` if the ID is `undefined` or not present in either party. + */ + getPokemonById(pokemonId: number | undefined): Pokemon | null { + if (isNullOrUndefined(pokemonId)) { + return null; + } + + const party = (this.getPlayerParty() as Pokemon[]).concat(this.getEnemyParty()); + return party.find(p => p.id === pokemonId) ?? null; } addPlayerPokemon( diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 494a0438b18..a1bb493bd5b 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -72,10 +72,11 @@ export abstract class ArenaTag { /** * Helper function that retrieves the source Pokemon - * @returns The source {@linkcode Pokemon} or `null` if none is found + * @returns - The source {@linkcode Pokemon} for this tag. + * Returns `null` if `this.sourceId` is `undefined` */ public getSourcePokemon(): Pokemon | null { - return this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; + return globalScene.getPokemonById(this.sourceId); } /** @@ -107,19 +108,22 @@ export class MistTag extends ArenaTag { onAdd(arena: Arena, quiet = false): void { super.onAdd(arena); - if (this.sourceId) { - const source = globalScene.getPokemonById(this.sourceId); - - if (!quiet && source) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:mistOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(source), - }), - ); - } else if (!quiet) { - console.warn("Failed to get source for MistTag onAdd"); - } + // We assume `quiet=true` means "just add the bloody tag no questions asked" + if (quiet) { + return; } + + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for MistTag on add message; id: ${this.sourceId}`); + return; + } + + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:mistOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(source), + }), + ); } /** @@ -440,18 +444,18 @@ class MatBlockTag extends ConditionalProtectTag { } onAdd(_arena: Arena) { - if (this.sourceId) { - const source = globalScene.getPokemonById(this.sourceId); - if (source) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:matBlockOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(source), - }), - ); - } else { - console.warn("Failed to get source for MatBlockTag onAdd"); - } + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for Mat Block message; id: ${this.sourceId}`); + return; } + + super.onAdd(_arena); + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:matBlockOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(source), + }), + ); } } @@ -511,7 +515,12 @@ export class NoCritTag extends ArenaTag { /** Queues a message upon removing this effect from the field */ onRemove(_arena: Arena): void { - const source = globalScene.getPokemonById(this.sourceId!); // TODO: is this bang correct? + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for NoCritTag on remove message; id: ${this.sourceId}`); + return; + } + globalScene.phaseManager.queueMessage( i18next.t("arenaTag:noCritOnRemove", { pokemonNameWithAffix: getPokemonNameWithAffix(source ?? undefined), @@ -522,7 +531,7 @@ export class NoCritTag extends ArenaTag { } /** - * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) Wish}. + * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) | Wish}. * Heals the Pokémon in the user's position the turn after Wish is used. */ class WishTag extends ArenaTag { @@ -535,18 +544,20 @@ class WishTag extends ArenaTag { } onAdd(_arena: Arena): void { - if (this.sourceId) { - const user = globalScene.getPokemonById(this.sourceId); - if (user) { - this.battlerIndex = user.getBattlerIndex(); - this.triggerMessage = i18next.t("arenaTag:wishTagOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(user), - }); - this.healHp = toDmgValue(user.getMaxHp() / 2); - } else { - console.warn("Failed to get source for WishTag onAdd"); - } + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for WishTag on add message; id: ${this.sourceId}`); + return; } + + super.onAdd(_arena); + this.healHp = toDmgValue(source.getMaxHp() / 2); + + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:wishTagOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(source), + }), + ); } onRemove(_arena: Arena): void { @@ -741,15 +752,23 @@ class SpikesTag extends ArenaTrapTag { onAdd(arena: Arena, quiet = false): void { super.onAdd(arena); - const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; - if (!quiet && source) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:spikesOnAdd", { - moveName: this.getMoveName(), - opponentDesc: source.getOpponentDescriptor(), - }), - ); + // We assume `quiet=true` means "just add the bloody tag no questions asked" + if (quiet) { + return; } + + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`); + return; + } + + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:spikesOnAdd", { + moveName: this.getMoveName(), + opponentDesc: source.getOpponentDescriptor(), + }), + ); } override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { @@ -794,15 +813,23 @@ class ToxicSpikesTag extends ArenaTrapTag { onAdd(arena: Arena, quiet = false): void { super.onAdd(arena); - const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; - if (!quiet && source) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:toxicSpikesOnAdd", { - moveName: this.getMoveName(), - opponentDesc: source.getOpponentDescriptor(), - }), - ); + if (quiet) { + // We assume `quiet=true` means "just add the bloody tag no questions asked" + return; } + + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for ToxicSpikesTag on add message; id: ${this.sourceId}`); + return; + } + + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:toxicSpikesOnAdd", { + moveName: this.getMoveName(), + opponentDesc: source.getOpponentDescriptor(), + }), + ); } onRemove(arena: Arena): void { @@ -905,7 +932,11 @@ class StealthRockTag extends ArenaTrapTag { onAdd(arena: Arena, quiet = false): void { super.onAdd(arena); - const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; + if (quiet) { + return; + } + + const source = this.getSourcePokemon(); if (!quiet && source) { globalScene.phaseManager.queueMessage( i18next.t("arenaTag:stealthRockOnAdd", { @@ -989,15 +1020,24 @@ class StickyWebTag extends ArenaTrapTag { onAdd(arena: Arena, quiet = false): void { super.onAdd(arena); - const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; - if (!quiet && source) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:stickyWebOnAdd", { - moveName: this.getMoveName(), - opponentDesc: source.getOpponentDescriptor(), - }), - ); + + // We assume `quiet=true` means "just add the bloody tag no questions asked" + if (quiet) { + return; } + + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`); + return; + } + + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:stickyWebOnAdd", { + moveName: this.getMoveName(), + opponentDesc: source.getOpponentDescriptor(), + }), + ); } override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { @@ -1061,14 +1101,20 @@ export class TrickRoomTag extends ArenaTag { } onAdd(_arena: Arena): void { - const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; - if (source) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:trickRoomOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(source), - }), - ); + super.onAdd(_arena); + + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for TrickRoomTag on add message; id: ${this.sourceId}`); + return; } + + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:trickRoomOnAdd", { + moveName: this.getMoveName(), + opponentDesc: source.getOpponentDescriptor(), + }), + ); } onRemove(_arena: Arena): void { @@ -1115,6 +1161,13 @@ class TailwindTag extends ArenaTag { } onAdd(_arena: Arena, quiet = false): void { + const source = this.getSourcePokemon(); + if (!source) { + return; + } + + super.onAdd(_arena, quiet); + if (!quiet) { globalScene.phaseManager.queueMessage( i18next.t( @@ -1123,15 +1176,14 @@ class TailwindTag extends ArenaTag { ); } - const source = globalScene.getPokemonById(this.sourceId!); //TODO: this bang is questionable! - const party = (source?.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField()) ?? []; - const phaseManager = globalScene.phaseManager; + const field = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - for (const pokemon of party) { + for (const pokemon of field) { // Apply the CHARGED tag to party members with the WIND_POWER ability + // TODO: This should not be handled here if (pokemon.hasAbility(AbilityId.WIND_POWER) && !pokemon.getTag(BattlerTagType.CHARGED)) { pokemon.addTag(BattlerTagType.CHARGED); - phaseManager.queueMessage( + globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:windPowerCharged", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: this.getMoveName(), @@ -1142,9 +1194,16 @@ class TailwindTag extends ArenaTag { // Raise attack by one stage if party member has WIND_RIDER ability // TODO: Ability displays should be handled by the ability if (pokemon.hasAbility(AbilityId.WIND_RIDER)) { - phaseManager.queueAbilityDisplay(pokemon, false, true); - phaseManager.unshiftNew("StatStageChangePhase", pokemon.getBattlerIndex(), true, [Stat.ATK], 1, true); - phaseManager.queueAbilityDisplay(pokemon, false, false); + globalScene.phaseManager.queueAbilityDisplay(pokemon, false, true); + globalScene.phaseManager.unshiftNew( + "StatStageChangePhase", + pokemon.getBattlerIndex(), + true, + [Stat.ATK], + 1, + true, + ); + globalScene.phaseManager.queueAbilityDisplay(pokemon, false, false); } } } @@ -1216,24 +1275,26 @@ class ImprisonTag extends ArenaTrapTag { } /** - * This function applies the effects of Imprison to the opposing Pokemon already present on the field. - * @param arena + * Apply the effects of Imprison to all opposing on-field Pokemon. */ override onAdd() { const source = this.getSourcePokemon(); - if (source) { - const party = this.getAffectedPokemon(); - party?.forEach((p: Pokemon) => { - if (p.isAllowedInBattle()) { - p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId); - } - }); - globalScene.phaseManager.queueMessage( - i18next.t("battlerTags:imprisonOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(source), - }), - ); + if (!source) { + return; } + + const party = this.getAffectedPokemon(); + party.forEach(p => { + if (p.isAllowedInBattle()) { + p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId); + } + }); + + globalScene.phaseManager.queueMessage( + i18next.t("battlerTags:imprisonOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(source), + }), + ); } /** @@ -1243,7 +1304,7 @@ class ImprisonTag extends ArenaTrapTag { */ override lapse(): boolean { const source = this.getSourcePokemon(); - return source ? source.isActive(true) : false; + return !!source?.isActive(true); } /** @@ -1265,9 +1326,7 @@ class ImprisonTag extends ArenaTrapTag { */ override onRemove(): void { const party = this.getAffectedPokemon(); - party?.forEach((p: Pokemon) => { - p.removeTag(BattlerTagType.IMPRISON); - }); + party.forEach(p => p.removeTag(BattlerTagType.IMPRISON)); } } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 8405fd1dd4d..cfc5c1b4ea9 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -111,7 +111,7 @@ export class BattlerTag { * @returns The source {@linkcode Pokemon}, or `null` if none is found */ public getSourcePokemon(): Pokemon | null { - return this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; + return globalScene.getPokemonById(this.sourceId); } } @@ -540,9 +540,13 @@ export class TrappedTag extends BattlerTag { } canAdd(pokemon: Pokemon): boolean { - const source = globalScene.getPokemonById(this.sourceId!)!; - const move = allMoves[this.sourceMove]; + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for TrappedTag canAdd; id: ${this.sourceId}`); + return false; + } + const move = allMoves[this.sourceMove]; const isGhost = pokemon.isOfType(PokemonType.GHOST); const isTrapped = pokemon.getTag(TrappedTag); const hasSubstitute = move.hitsSubstitute(source, pokemon); @@ -763,12 +767,20 @@ export class DestinyBondTag extends BattlerTag { if (lapseType !== BattlerTagLapseType.CUSTOM) { return super.lapse(pokemon, lapseType); } - const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; - if (!source?.isFainted()) { + + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for DestinyBondTag lapse; id: ${this.sourceId}`); + return false; + } + + // Destiny bond stays active until the user faints + if (!source.isFainted()) { return true; } - if (source?.getAlly() === pokemon) { + // Don't kill allies or opposing bosses. + if (source.getAlly() === pokemon) { return false; } @@ -781,6 +793,7 @@ export class DestinyBondTag extends BattlerTag { return false; } + // Drag the foe down with the user globalScene.phaseManager.queueMessage( i18next.t("battlerTags:destinyBondLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(source), @@ -798,17 +811,13 @@ export class InfatuatedTag extends BattlerTag { } canAdd(pokemon: Pokemon): boolean { - if (this.sourceId) { - const pkm = globalScene.getPokemonById(this.sourceId); - - if (pkm) { - return pokemon.isOppositeGender(pkm); - } - console.warn("canAdd: this.sourceId is not a valid pokemon id!", this.sourceId); + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for InfatuatedTag canAdd; id: ${this.sourceId}`); return false; } - console.warn("canAdd: this.sourceId is undefined"); - return false; + + return pokemon.isOppositeGender(source); } onAdd(pokemon: Pokemon): void { @@ -817,7 +826,7 @@ export class InfatuatedTag extends BattlerTag { globalScene.phaseManager.queueMessage( i18next.t("battlerTags:infatuatedOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + sourcePokemonName: getPokemonNameWithAffix(this.getSourcePokemon()!), // Tag not added + console warns if no source }), ); } @@ -835,28 +844,36 @@ export class InfatuatedTag extends BattlerTag { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); - const phaseManager = globalScene.phaseManager; - - if (ret) { - phaseManager.queueMessage( - i18next.t("battlerTags:infatuatedLapse", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? - }), - ); - phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT); - - if (pokemon.randBattleSeedInt(2)) { - phaseManager.queueMessage( - i18next.t("battlerTags:infatuatedLapseImmobilize", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - ); - (phaseManager.getCurrentPhase() as MovePhase).cancel(); - } + if (!ret) { + return false; } - return ret; + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for InfatuatedTag lapse; id: ${this.sourceId}`); + return false; + } + + const phaseManager = globalScene.phaseManager; + phaseManager.queueMessage( + i18next.t("battlerTags:infatuatedLapse", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + }), + ); + phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT); + + // 50% chance to disrupt the target's action + if (pokemon.randBattleSeedInt(2)) { + phaseManager.queueMessage( + i18next.t("battlerTags:infatuatedLapseImmobilize", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }), + ); + (phaseManager.getCurrentPhase() as MovePhase).cancel(); + } + + return true; } onRemove(pokemon: Pokemon): void { @@ -899,6 +916,12 @@ export class SeedTag extends BattlerTag { } onAdd(pokemon: Pokemon): void { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for SeedTag onAdd; id: ${this.sourceId}`); + return; + } + super.onAdd(pokemon); globalScene.phaseManager.queueMessage( @@ -906,47 +929,51 @@ export class SeedTag extends BattlerTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct? + this.sourceIndex = source.getBattlerIndex(); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); - if (ret) { - const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); - if (source) { - const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); - - if (!cancelled.value) { - globalScene.phaseManager.unshiftNew( - "CommonAnimPhase", - source.getBattlerIndex(), - pokemon.getBattlerIndex(), - CommonAnim.LEECH_SEED, - ); - - const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); - const reverseDrain = pokemon.hasAbilityWithAttr("ReverseDrainAbAttr", false); - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - source.getBattlerIndex(), - !reverseDrain ? damage : damage * -1, - !reverseDrain - ? i18next.t("battlerTags:seededLapse", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }) - : i18next.t("battlerTags:seededLapseShed", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - false, - true, - ); - } - } + if (!ret) { + return false; } - return ret; + // Check which opponent to restore HP to + const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); + if (!source) { + console.warn(`Failed to get source Pokemon for SeedTag lapse; id: ${this.sourceId}`); + return false; + } + + const cancelled = new BooleanHolder(false); + applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); + + if (cancelled.value) { + return true; + } + + globalScene.phaseManager.unshiftNew( + "CommonAnimPhase", + source.getBattlerIndex(), + pokemon.getBattlerIndex(), + CommonAnim.LEECH_SEED, + ); + + // Damage the target and restore our HP (or take damage in the case of liquid ooze) + const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); + const reverseDrain = pokemon.hasAbilityWithAttr("ReverseDrainAbAttr", false); + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + source.getBattlerIndex(), + reverseDrain ? -damage : damage, + i18next.t(reverseDrain ? "battlerTags:seededLapseShed" : "battlerTags:seededLapse", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }), + false, + true, + ); + return true; } getDescriptor(): string { @@ -1195,9 +1222,15 @@ export class HelpingHandTag extends BattlerTag { } onAdd(pokemon: Pokemon): void { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for HelpingHandTag onAdd; id: ${this.sourceId}`); + return; + } + globalScene.phaseManager.queueMessage( i18next.t("battlerTags:helpingHandOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + pokemonNameWithAffix: getPokemonNameWithAffix(source), pokemonName: getPokemonNameWithAffix(pokemon), }), ); @@ -1219,9 +1252,7 @@ export class IngrainTag extends TrappedTag { * @returns boolean True if the tag can be added, false otherwise */ canAdd(pokemon: Pokemon): boolean { - const isTrapped = pokemon.getTag(BattlerTagType.TRAPPED); - - return !isTrapped; + return !pokemon.getTag(BattlerTagType.TRAPPED); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -1420,15 +1451,22 @@ export abstract class DamagingTrapTag extends TrappedTag { } } +// TODO: Condense all these tags into 1 singular tag with a modified message func export class BindTag extends DamagingTrapTag { constructor(turnCount: number, sourceId: number) { super(BattlerTagType.BIND, CommonAnim.BIND, turnCount, MoveId.BIND, sourceId); } getTrapMessage(pokemon: Pokemon): string { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for BindTag getTrapMessage; id: ${this.sourceId}`); + return "ERROR - CHECK CONSOLE AND REPORT"; + } + return i18next.t("battlerTags:bindOnTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + sourcePokemonName: getPokemonNameWithAffix(source), moveName: this.getMoveName(), }); } @@ -1440,9 +1478,16 @@ export class WrapTag extends DamagingTrapTag { } getTrapMessage(pokemon: Pokemon): string { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for WrapTag getTrapMessage; id: ${this.sourceId}`); + return "ERROR - CHECK CONSOLE AND REPORT"; + } + return i18next.t("battlerTags:wrapOnTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + sourcePokemonName: getPokemonNameWithAffix(source), + moveName: this.getMoveName(), }); } } @@ -1473,8 +1518,14 @@ export class ClampTag extends DamagingTrapTag { } getTrapMessage(pokemon: Pokemon): string { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for ClampTag getTrapMessage; id: ${this.sourceId}`); + return "ERROR - CHECK CONSOLE AND REPORT ASAP"; + } + return i18next.t("battlerTags:clampOnTrap", { - sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + sourcePokemonNameWithAffix: getPokemonNameWithAffix(source), pokemonName: getPokemonNameWithAffix(pokemon), }); } @@ -1523,9 +1574,15 @@ export class ThunderCageTag extends DamagingTrapTag { } getTrapMessage(pokemon: Pokemon): string { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for ThunderCageTag getTrapMessage; id: ${this.sourceId}`); + return "ERROR - PLEASE REPORT ASAP"; + } + return i18next.t("battlerTags:thunderCageOnTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + sourcePokemonNameWithAffix: getPokemonNameWithAffix(source), }); } } @@ -1536,9 +1593,15 @@ export class InfestationTag extends DamagingTrapTag { } getTrapMessage(pokemon: Pokemon): string { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for InfestationTag getTrapMessage; id: ${this.sourceId}`); + return "ERROR - CHECK CONSOLE AND REPORT"; + } + return i18next.t("battlerTags:infestationOnTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + sourcePokemonNameWithAffix: getPokemonNameWithAffix(source), }); } } @@ -2221,14 +2284,19 @@ export class SaltCuredTag extends BattlerTag { } onAdd(pokemon: Pokemon): void { - super.onAdd(pokemon); + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for SaltCureTag onAdd; id: ${this.sourceId}`); + return; + } + super.onAdd(pokemon); globalScene.phaseManager.queueMessage( i18next.t("battlerTags:saltCuredOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct? + this.sourceIndex = source.getBattlerIndex(); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -2281,8 +2349,14 @@ export class CursedTag extends BattlerTag { } onAdd(pokemon: Pokemon): void { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for CursedTag onAdd; id: ${this.sourceId}`); + return; + } + super.onAdd(pokemon); - this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct? + this.sourceIndex = source.getBattlerIndex(); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -2902,7 +2976,13 @@ export class SubstituteTag extends BattlerTag { /** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */ onAdd(pokemon: Pokemon): void { - this.hp = Math.floor(globalScene.getPokemonById(this.sourceId!)!.getMaxHp() / 4); + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for SubstituteTag onAdd; id: ${this.sourceId}`); + return; + } + + this.hp = Math.floor(source.getMaxHp() / 4); this.sourceInFocus = false; // Queue battle animation and message @@ -3182,13 +3262,14 @@ export class ImprisonTag extends MoveRestrictionBattlerTag { */ public override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { const source = this.getSourcePokemon(); - if (source) { - if (lapseType === BattlerTagLapseType.PRE_MOVE) { - return super.lapse(pokemon, lapseType) && source.isActive(true); - } - return source.isActive(true); + if (!source) { + console.warn(`Failed to get source Pokemon for ImprisonTag lapse; id: ${this.sourceId}`); + return false; } - return false; + if (lapseType === BattlerTagLapseType.PRE_MOVE) { + return super.lapse(pokemon, lapseType) && source.isActive(true); + } + return source.isActive(true); } /** @@ -3248,12 +3329,20 @@ export class SyrupBombTag extends BattlerTag { * Applies the single-stage speed down to the target Pokemon and decrements the tag's turn count * @param pokemon - The target {@linkcode Pokemon} * @param _lapseType - N/A - * @returns `true` if the `turnCount` is still greater than `0`; `false` if the `turnCount` is `0` or the target or source Pokemon has been removed from the field + * @returns Whether the tag should persist (`turnsRemaining > 0` and source still on field) */ override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { - if (this.sourceId && !globalScene.getPokemonById(this.sourceId)?.isActive(true)) { + const source = this.getSourcePokemon(); + if (!source) { + console.warn(`Failed to get source Pokemon for SyrupBombTag lapse; id: ${this.sourceId}`); return false; } + + // Syrup bomb clears immediately if source leaves field/faints + if (!source.isActive(true)) { + return false; + } + // Custom message in lieu of an animation in mainline globalScene.phaseManager.queueMessage( i18next.t("battlerTags:syrupBombLapse", { @@ -3270,7 +3359,7 @@ export class SyrupBombTag extends BattlerTag { false, true, ); - return --this.turnCount > 0; + return super.lapse(pokemon, _lapseType); } } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index f94c59bb463..4caa9f434bb 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6887,12 +6887,12 @@ export class RandomMovesetMoveAttr extends CallMoveAttr { // includeParty will be true for Assist, false for Sleep Talk let allies: Pokemon[]; if (this.includeParty) { - allies = user.isPlayer() ? globalScene.getPlayerParty().filter(p => p !== user) : globalScene.getEnemyParty().filter(p => p !== user); + allies = (user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user); } else { allies = [ user ]; } - const partyMoveset = allies.map(p => p.moveset).flat(); - const moves = partyMoveset.filter(m => !this.invalidMoves.has(m!.moveId) && !m!.getMove().name.endsWith(" (N)")); + const partyMoveset = allies.flatMap(p => p.moveset); + const moves = partyMoveset.filter(m => !this.invalidMoves.has(m.moveId) && !m.getMove().name.endsWith(" (N)")); if (moves.length === 0) { return false; } diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index c68e395b379..cf24d1dd7e0 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -328,7 +328,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder .withOptionPhase(async () => { // Show the Oricorio a dance, and recruit it const encounter = globalScene.currentBattle.mysteryEncounter!; - const oricorio = encounter.misc.oricorioData.toPokemon(); + const oricorio = encounter.misc.oricorioData.toPokemon() as EnemyPokemon; oricorio.passive = true; // Ensure the Oricorio's moveset gains the Dance move the player used diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e9cc4f70d70..3080566b815 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4100,7 +4100,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getTag(tagType: Constructor): T | undefined; getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { - return tagType instanceof Function + return typeof tagType === "function" ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType); } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 54b7323569a..77d82c2a694 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -751,7 +751,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { } getPokemon(): Pokemon | undefined { - return this.pokemonId ? (globalScene.getPokemonById(this.pokemonId) ?? undefined) : undefined; + return globalScene.getPokemonById(this.pokemonId) ?? undefined; } getScoreMultiplier(): number { diff --git a/test/abilities/unburden.test.ts b/test/abilities/unburden.test.ts index 6e24e10d168..4bf12d01ad6 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -22,10 +22,7 @@ describe("Abilities - Unburden", () => { */ function getHeldItemCount(pokemon: Pokemon): number { const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount()); - if (stackCounts.length) { - return stackCounts.reduce((a, b) => a + b); - } - return 0; + return stackCounts.reduce((a, b) => a + b, 0); } beforeAll(() => { diff --git a/test/field/pokemon-id-checks.test.ts b/test/field/pokemon-id-checks.test.ts new file mode 100644 index 00000000000..4023b8d73ad --- /dev/null +++ b/test/field/pokemon-id-checks.test.ts @@ -0,0 +1,79 @@ +import type Pokemon from "#app/field/pokemon"; +import { MoveId } from "#enums/move-id"; +import { AbilityId } from "#enums/ability-id"; +import { SpeciesId } from "#enums/species-id"; +import { BattleType } from "#enums/battle-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { BattlerIndex } from "#enums/battler-index"; + +describe("Field - Pokemon ID Checks", () => { + 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 + .ability(AbilityId.NO_GUARD) + .battleStyle("single") + .battleType(BattleType.TRAINER) + .criticalHits(false) + .enemyLevel(100) + .enemySpecies(SpeciesId.ARCANINE) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH); + }); + + function onlyUnique(array: T[]): T[] { + return [...new Set(array)]; + } + + // TODO: We currently generate IDs as a pure random integer; enable once unique UUIDs are added + it.todo("2 Pokemon should not be able to generate with the same ID during 1 encounter", async () => { + game.override.battleType(BattleType.TRAINER); // enemy generates 2 mons + await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.ABRA]); + + const ids = (game.scene.getPlayerParty() as Pokemon[]).concat(game.scene.getEnemyParty()).map((p: Pokemon) => p.id); + const uniqueIds = onlyUnique(ids); + + expect(ids).toHaveLength(uniqueIds.length); + }); + + it("should not prevent Battler Tags from triggering if user has PID of 0", async () => { + await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.AERODACTYL]); + + const player = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); + // Override player pokemon PID to be 0 + player.id = 0; + expect(player.getTag(BattlerTagType.DESTINY_BOND)).toBeUndefined(); + + game.move.use(MoveId.DESTINY_BOND); + game.doSelectPartyPokemon(1); + await game.move.forceEnemyMove(MoveId.FLAME_WHEEL); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("MoveEndPhase"); + + const dBondTag = player.getTag(BattlerTagType.DESTINY_BOND)!; + expect(dBondTag).toBeDefined(); + expect(dBondTag.sourceId).toBe(0); + expect(dBondTag.getSourcePokemon()).toBe(player); + + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(player.isFainted()).toBe(true); + expect(enemy.isFainted()).toBe(true); + }); +});