diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 8405fd1dd4d..245c058ab56 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -669,7 +669,12 @@ export class ConfusedTag extends BattlerTag { } canAdd(pokemon: Pokemon): boolean { - return globalScene.arena.terrain?.terrainType !== TerrainType.MISTY || !pokemon.isGrounded(); + const blockedByTerrain = pokemon.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.MISTY; + if (blockedByTerrain) { + pokemon.queueStatusImmuneMessage(false, TerrainType.MISTY); + return false; + } + return true; } onAdd(pokemon: Pokemon): void { diff --git a/src/data/terrain.ts b/src/data/terrain.ts index b3ee62ac2f9..bcb28558de6 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -3,6 +3,7 @@ import type Move from "./moves/move"; import { PokemonType } from "#enums/pokemon-type"; import type { BattlerIndex } from "#enums/battler-index"; import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; export enum TerrainType { NONE, @@ -96,3 +97,76 @@ export function getTerrainColor(terrainType: TerrainType): [number, number, numb return [0, 0, 0]; } + +/** + * Return the message associated with a terrain effect starting. + * @param terrainType - The {@linkcode TerrainType} starting. + * @returns A string containing the appropriate terrain start text. + */ +export function getTerrainStartMessage(terrainType: TerrainType): string { + switch (terrainType) { + case TerrainType.MISTY: + return i18next.t("terrain:mistyStartMessage"); + case TerrainType.ELECTRIC: + return i18next.t("terrain:electricStartMessage"); + case TerrainType.GRASSY: + return i18next.t("terrain:grassyStartMessage"); + case TerrainType.PSYCHIC: + return i18next.t("terrain:psychicStartMessage"); + case TerrainType.NONE: + default: + terrainType satisfies TerrainType.NONE; + console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainStartMessage!`); + return ""; + } +} + +/** + * Return the message associated with a terrain effect ceasing to exist. + * @param terrainType - The {@linkcode TerrainType} being cleared. + * @returns A string containing the appropriate terrain clear text. + */ +export function getTerrainClearMessage(terrainType: TerrainType): string { + switch (terrainType) { + case TerrainType.MISTY: + return i18next.t("terrain:mistyClearMessage"); + case TerrainType.ELECTRIC: + return i18next.t("terrain:electricClearMessage"); + case TerrainType.GRASSY: + return i18next.t("terrain:grassyClearMessage"); + case TerrainType.PSYCHIC: + return i18next.t("terrain:psychicClearMessage"); + case TerrainType.NONE: + default: + terrainType satisfies TerrainType.NONE; + console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainClearMessage!`); + return ""; + } +} + +/** + * Return the message associated with a terrain-induced move/effect blockage. + * @param pokemon - The {@linkcode Pokemon} being protected. + * @param terrainType - The {@linkcode TerrainType} in question + * @returns A string containing the appropriate terrain block text. + */ +export function getTerrainBlockMessage(pokemon: Pokemon, terrainType: TerrainType): string { + switch (terrainType) { + case TerrainType.MISTY: + return i18next.t("terrain:mistyBlockMessage", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }); + case TerrainType.ELECTRIC: + case TerrainType.GRASSY: + case TerrainType.PSYCHIC: + return i18next.t("terrain:defaultBlockMessage", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + terrainName: getTerrainName(terrainType), + }); + case TerrainType.NONE: + default: + terrainType satisfies TerrainType.NONE; + console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainBlockMessage!`); + return ""; + } +} diff --git a/src/data/weather.ts b/src/data/weather.ts index 425e15b12a8..3fde932a1d7 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -5,7 +5,6 @@ import type Pokemon from "../field/pokemon"; import { PokemonType } from "#enums/pokemon-type"; import type Move from "./moves/move"; import { randSeedInt } from "#app/utils/common"; -import { TerrainType, getTerrainName } from "./terrain"; import i18next from "i18next"; import { globalScene } from "#app/global-scene"; import type { Arena } from "#app/field/arena"; @@ -235,50 +234,6 @@ export function getWeatherBlockMessage(weatherType: WeatherType): string { return i18next.t("weather:defaultEffectMessage"); } -export function getTerrainStartMessage(terrainType: TerrainType): string | null { - switch (terrainType) { - case TerrainType.MISTY: - return i18next.t("terrain:mistyStartMessage"); - case TerrainType.ELECTRIC: - return i18next.t("terrain:electricStartMessage"); - case TerrainType.GRASSY: - return i18next.t("terrain:grassyStartMessage"); - case TerrainType.PSYCHIC: - return i18next.t("terrain:psychicStartMessage"); - default: - console.warn("getTerrainStartMessage not defined. Using default null"); - return null; - } -} - -export function getTerrainClearMessage(terrainType: TerrainType): string | null { - switch (terrainType) { - case TerrainType.MISTY: - return i18next.t("terrain:mistyClearMessage"); - case TerrainType.ELECTRIC: - return i18next.t("terrain:electricClearMessage"); - case TerrainType.GRASSY: - return i18next.t("terrain:grassyClearMessage"); - case TerrainType.PSYCHIC: - return i18next.t("terrain:psychicClearMessage"); - default: - console.warn("getTerrainClearMessage not defined. Using default null"); - return null; - } -} - -export function getTerrainBlockMessage(pokemon: Pokemon, terrainType: TerrainType): string { - if (terrainType === TerrainType.MISTY) { - return i18next.t("terrain:mistyBlockMessage", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }); - } - return i18next.t("terrain:defaultBlockMessage", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - terrainName: getTerrainName(terrainType), - }); -} - export interface WeatherPoolEntry { weatherType: WeatherType; weight: number; diff --git a/src/field/arena.ts b/src/field/arena.ts index 8d7e5037852..d13335527be 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -5,8 +5,6 @@ import { randSeedInt, NumberHolder, isNullOrUndefined, type Constructor } from " import type PokemonSpecies from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { - getTerrainClearMessage, - getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, getLegendaryWeatherContinuesMessage, @@ -19,7 +17,7 @@ import type { ArenaTag } from "#app/data/arena-tag"; import { ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; import { ArenaTagSide } from "#enums/arena-tag-side"; import type { BattlerIndex } from "#enums/battler-index"; -import { Terrain, TerrainType } from "#app/data/terrain"; +import { Terrain, TerrainType, getTerrainClearMessage, getTerrainStartMessage } from "#app/data/terrain"; import { applyAbAttrs, applyPostTerrainChangeAbAttrs, @@ -449,9 +447,9 @@ export class Arena { CommonAnim.MISTY_TERRAIN + (terrain - 1), ); } - globalScene.phaseManager.queueMessage(getTerrainStartMessage(terrain)!); // TODO: is this bang correct? + globalScene.phaseManager.queueMessage(getTerrainStartMessage(terrain)); } else { - globalScene.phaseManager.queueMessage(getTerrainClearMessage(oldTerrainType)!); // TODO: is this bang correct? + globalScene.phaseManager.queueMessage(getTerrainClearMessage(oldTerrainType)); } globalScene diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e9cc4f70d70..61d9afec21f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -184,12 +184,13 @@ import { timedEventManager } from "#app/global-event-manager"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode"; import { FieldPosition } from "#enums/field-position"; -import { LearnMoveSituation } from "#enums/learn-move-situation"; import { HitResult } from "#enums/hit-result"; import { AiType } from "#enums/ai-type"; import type { MoveResult } from "#enums/move-result"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import type { AbAttrMap, AbAttrString } from "#app/@types/ability-types"; +import { getTerrainBlockMessage } from "#app/data/terrain"; +import { LearnMoveSituation } from "#enums/learn-move-situation"; /** Base typeclass for damage parameter methods, used for DRY */ type damageParams = { @@ -4574,16 +4575,34 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ); } - queueImmuneMessage(quiet: boolean, effect?: StatusEffect): void { - if (!effect || quiet) { + /** + * 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 - + * can be "overlap" (already has same status), "other" (generic fail message) + * or a {@linkcode TerrainType} for terrain-based blockages. + * Defaults to "other". + */ + queueStatusImmuneMessage(quiet: boolean, reason: "overlap" | "other" | TerrainType = "other"): void { + if (quiet) { return; } - const message = - effect && this.status?.effect === effect - ? getStatusEffectOverlapText(effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this)) - : i18next.t("abilityTriggers:moveImmunity", { - pokemonNameWithAffix: getPokemonNameWithAffix(this), - }); + + let message: string; + if (reason === "overlap") { + // "XYZ is already XXX!" + message = getStatusEffectOverlapText(this.status?.effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this)); + } else if (typeof reason === "number") { + // "XYZ was protected by the XXX terrain!" / + // "XYZ surrounds itself with a protective mist!" + message = getTerrainBlockMessage(this, reason); + } else { + // "It doesn't affect XXX!" + message = i18next.t("abilityTriggers:moveImmunity", { + pokemonNameWithAffix: getPokemonNameWithAffix(this), + }); + } + globalScene.phaseManager.queueMessage(message); } @@ -4605,11 +4624,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ): boolean { if (effect !== StatusEffect.FAINT) { if (overrideStatus ? this.status?.effect === effect : this.status) { - this.queueImmuneMessage(quiet, effect); + this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message return false; } if (this.isGrounded() && !ignoreField && globalScene.arena.terrain?.terrainType === TerrainType.MISTY) { - this.queueImmuneMessage(quiet, effect); + this.queueStatusImmuneMessage(quiet, TerrainType.MISTY); return false; } } @@ -4640,7 +4659,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) { if (poisonImmunity.includes(true)) { - this.queueImmuneMessage(quiet, effect); + this.queueStatusImmuneMessage(quiet); return false; } } @@ -4648,13 +4667,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } case StatusEffect.PARALYSIS: if (this.isOfType(PokemonType.ELECTRIC)) { - this.queueImmuneMessage(quiet, effect); + this.queueStatusImmuneMessage(quiet); return false; } break; case StatusEffect.SLEEP: if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) { - this.queueImmuneMessage(quiet, effect); + this.queueStatusImmuneMessage(quiet, TerrainType.ELECTRIC); return false; } break; @@ -4665,13 +4684,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { globalScene?.arena?.weather?.weatherType && [WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType)) ) { - this.queueImmuneMessage(quiet, effect); + this.queueStatusImmuneMessage(quiet); return false; } break; case StatusEffect.BURN: if (this.isOfType(PokemonType.FIRE)) { - this.queueImmuneMessage(quiet, effect); + this.queueStatusImmuneMessage(quiet); return false; } break; diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 2e94b085948..5fd161fc7a9 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -11,7 +11,7 @@ import { MoveFlags } from "#enums/MoveFlags"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { PokemonType } from "#enums/pokemon-type"; -import { getTerrainBlockMessage, getWeatherBlockMessage } from "#app/data/weather"; +import { getWeatherBlockMessage } from "#app/data/weather"; import { MoveUsedEvent } from "#app/events/battle-scene"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; @@ -26,6 +26,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; +import { getTerrainBlockMessage } from "#app/data/terrain"; import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode"; import { frenzyMissFunc } from "#app/data/moves/move-utils";