From 6a284b7ab32cdb7e88bb48eda191ada6063620cf Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 18:30:41 -0400 Subject: [PATCH] Added `MessageAttr`; cleaned up a lot of other jank move attrs --- src/@types/move-types.ts | 11 +++ src/data/abilities/ability.ts | 1 + src/data/battler-tags.ts | 27 +------ src/data/moves/move.ts | 138 ++++++++++++++++------------------ 4 files changed, 79 insertions(+), 98 deletions(-) diff --git a/src/@types/move-types.ts b/src/@types/move-types.ts index 5f8d7e8777e..e795ddbcd65 100644 --- a/src/@types/move-types.ts +++ b/src/@types/move-types.ts @@ -1,13 +1,24 @@ +import type { Pokemon } from "#field/pokemon"; import type { AttackMove, ChargingAttackMove, ChargingSelfStatusMove, + Move, MoveAttr, MoveAttrConstructorMap, SelfStatusMove, StatusMove, } from "#moves/move"; +/** + * A generic function producing a message during a Move's execution. + * @param user - The {@linkcode Pokemon} using the move + * @param target - The {@linkcode Pokemon} targeted by the move + * @param move - The {@linkcode Move} being used + * @returns a string + */ +export type MoveMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => string; + export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type * from "#moves/move"; diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 0ee1a51a78e..c7356136541 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1670,6 +1670,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr { constructor( private newType: PokemonType, private powerMultiplier: number, + // TODO: all moves with this attr solely check the move being used... private condition?: PokemonAttackCondition, ) { super(false); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 2d22ab84e8c..2f540f5e1b9 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -911,29 +911,6 @@ export class DestinyBondTag extends SerializableBattlerTag { } } -/** - * Tag added by {@linkcode MoveId.LASER_FOCUS} to cause the user's attacks to always critically strike - * until the end of the next turn. - */ -export class LaserFocusTag extends SerializableBattlerTag { - public override readonly tagType = BattlerTagType.ALWAYS_CRIT; - - constructor() { - // TODO: Is this per attack or per turn? - super(BattlerTagType.ALWAYS_CRIT, BattlerTagLapseType.TURN_END, 2, MoveId.LASER_FOCUS); - } - - override onAdd(pokemon: Pokemon): void { - super.onAdd(pokemon); - - globalScene.phaseManager.queueMessage( - i18next.t("battlerTags:laserFocusOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - ); - } -} - // Technically serializable as in a double battle, a pokemon could be infatuated by its ally export class InfatuatedTag extends SerializableBattlerTag { public override readonly tagType = BattlerTagType.INFATUATED; @@ -3583,6 +3560,7 @@ export class GrudgeTag extends SerializableBattlerTag { * @param sourcePokemon - The source of the move that fainted the tag's bearer * @returns `false` if Grudge activates its effect or lapses */ + // TODO: Confirm whether this should interact with copying moves override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean { if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) { if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) { @@ -3770,7 +3748,6 @@ export function getBattlerTag( case BattlerTagType.DRAGON_CHEER: return new CritBoostTag(tagType, sourceMove); case BattlerTagType.ALWAYS_CRIT: - return new LaserFocusTag(); case BattlerTagType.IGNORE_ACCURACY: return new SerializableBattlerTag(tagType, BattlerTagLapseType.TURN_END, 2, sourceMove); case BattlerTagType.ALWAYS_GET_HIT: @@ -3943,7 +3920,7 @@ export type BattlerTagTypeMap = { [BattlerTagType.FIRE_BOOST]: TypeBoostTag; [BattlerTagType.CRIT_BOOST]: CritBoostTag; [BattlerTagType.DRAGON_CHEER]: CritBoostTag; - [BattlerTagType.ALWAYS_CRIT]: LaserFocusTag; + [BattlerTagType.ALWAYS_CRIT]: GenericSerializableBattlerTag; [BattlerTagType.IGNORE_ACCURACY]: GenericSerializableBattlerTag; [BattlerTagType.ALWAYS_GET_HIT]: GenericSerializableBattlerTag; [BattlerTagType.RECEIVE_DOUBLE_DAMAGE]: GenericSerializableBattlerTag; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index bde5f2977d8..1abe0407ddf 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -87,7 +87,7 @@ import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import type { AttackMoveResult } from "#types/attack-move-result"; import type { Localizable } from "#types/locales"; -import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types"; +import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types"; import type { TurnMove } from "#types/turn-move"; import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { getEnumValues } from "#utils/enums"; @@ -1357,20 +1357,20 @@ export class MoveHeaderAttr extends MoveAttr { /** * Header attribute to queue a message at the beginning of a turn. - * @see {@link MoveHeaderAttr} */ export class MessageHeaderAttr extends MoveHeaderAttr { - private message: string | ((user: Pokemon, move: Move) => string); + /** The message to display, or a function producing one. */ + private message: string | MoveMessageFunc; - constructor(message: string | ((user: Pokemon, move: Move) => string)) { + constructor(message: string | MoveMessageFunc) { super(); this.message = message; } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + apply(user: Pokemon, target: Pokemon, move: Move): boolean { const message = typeof this.message === "string" ? this.message - : this.message(user, move); + : this.message(user, target, move); if (message) { globalScene.phaseManager.queueMessage(message); @@ -1418,21 +1418,21 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr { */ export class PreMoveMessageAttr extends MoveAttr { /** The message to display or a function returning one */ - private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined); + private message: string | MoveMessageFunc; /** * Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution. - * @param message - The message to display before move use, either as a string or a function producing one. + * @param message - The message to display before move use, either` a literal string or a function producing one. * @remarks - * If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed + * If {@linkcode message} evaluates to an empty string (`""`), no message will be displayed * (though the move will still succeed). */ - constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) { + constructor(message: string | MoveMessageFunc) { super(); this.message = message; } - apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { + apply(user: Pokemon, target: Pokemon, move: Move): boolean { const message = typeof this.message === "function" ? this.message(user, target, move) : this.message; @@ -1453,18 +1453,17 @@ export class PreMoveMessageAttr extends MoveAttr { * @extends MoveAttr */ export class PreUseInterruptAttr extends MoveAttr { - protected message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string); - protected overridesFailedMessage: boolean; + protected message: string | MoveMessageFunc; protected conditionFunc: MoveConditionFunc; /** * Create a new MoveInterruptedMessageAttr. * @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move. */ - constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) { + constructor(message: string | MoveMessageFunc, conditionFunc: MoveConditionFunc) { super(); this.message = message; - this.conditionFunc = conditionFunc ?? (() => true); + this.conditionFunc = conditionFunc; } /** @@ -1485,11 +1484,9 @@ export class PreUseInterruptAttr extends MoveAttr { */ override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined { if (this.message && this.conditionFunc(user, target, move)) { - const message = - typeof this.message === "string" - ? (this.message as string) + return typeof this.message === "string" + ? this.message : this.message(user, target, move); - return message; } } } @@ -1694,19 +1691,33 @@ export class SurviveDamageAttr extends ModifiedDamageAttr { } } -export class SplashAttr extends MoveEffectAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:splash")); - return true; +/** + * Move attribute to display arbitrary text during a move's execution. + */ +export class MessageAttr extends MoveEffectAttr { + /** The message to display, either as a string or a function returning one. */ + private message: string | MoveMessageFunc; + + constructor(message: string | MoveMessageFunc, options?: MoveEffectAttrOptions) { + // TODO: Do we need to respect `selfTarget` if we're just displaying text? + super(false, options) + this.message = message; + } + + override apply(user: Pokemon, target: Pokemon, move: Move): boolean { + const message = typeof this.message === "function" + ? this.message(user, target, move) + : this.message; + + // TODO: Consider changing if/when MoveAttr `apply` return values become significant + if (message) { + globalScene.phaseManager.queueMessage(message, 500); + return true; + } + return false; } } -export class CelebrateAttr extends MoveEffectAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username })); - return true; - } -} export class RecoilAttr extends MoveEffectAttr { private useHp: boolean; @@ -5930,38 +5941,6 @@ export class ProtectAttr extends AddBattlerTagAttr { } } -export class IgnoreAccuracyAttr extends AddBattlerTagAttr { - constructor() { - super(BattlerTagType.IGNORE_ACCURACY, true, false, 2); - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!super.apply(user, target, move, args)) { - return false; - } - - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); - - return true; - } -} - -export class FaintCountdownAttr extends AddBattlerTagAttr { - constructor() { - super(BattlerTagType.PERISH_SONG, false, true, 4); - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!super.apply(user, target, move, args)) { - return false; - } - - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: this.turnCountMin - 1 })); - - return true; - } -} - /** * Attribute to remove all Substitutes from the field. * @extends MoveEffectAttr @@ -6602,8 +6581,10 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr { return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move); } } + export class RemoveTypeAttr extends MoveEffectAttr { + // TODO: Remove the message callback private removedType: PokemonType; private messageCallback: ((user: Pokemon) => void) | undefined; @@ -8292,8 +8273,6 @@ const MoveAttrs = Object.freeze({ RandomLevelDamageAttr, ModifiedDamageAttr, SurviveDamageAttr, - SplashAttr, - CelebrateAttr, RecoilAttr, SacrificialAttr, SacrificialAttrOnHit, @@ -8436,8 +8415,7 @@ const MoveAttrs = Object.freeze({ RechargeAttr, TrapAttr, ProtectAttr, - IgnoreAccuracyAttr, - FaintCountdownAttr, + MessageAttr, RemoveAllSubstitutesAttr, HitsTagAttr, HitsTagForDoubleDamageAttr, @@ -8931,7 +8909,7 @@ export function initMoves() { new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) .attr(RandomLevelDamageAttr), new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1) - .attr(SplashAttr) + .attr(MessageAttr, i18next.t("moveTriggers:splash")) .condition(failOnGravityCondition), new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), @@ -8993,7 +8971,10 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) .reflectable(), new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(IgnoreAccuracyAttr), + .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2) + .attr(MessageAttr, (user, target) => + i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }) + ), new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE) .condition(targetSleptOrComatoseCondition), @@ -9081,7 +9062,9 @@ export function initMoves() { return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS; }), new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(FaintCountdownAttr) + .attr(AddBattlerTagAttr, BattlerTagType.PERISH_SONG, false, true, 4) + .attr(MessageAttr, (_user, target) => + i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: 3 })) .ignoresProtect() .soundBased() .condition(failOnBossCondition) @@ -9097,7 +9080,10 @@ export function initMoves() { .attr(MultiHitAttr) .makesContact(false), new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(IgnoreAccuracyAttr), + .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2) + .attr(MessageAttr, (user, target) => + i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }) + ), new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2) .attr(FrenzyAttr) .attr(MissEffectAttr, frenzyMissFunc) @@ -9324,8 +9310,8 @@ export function initMoves() { && (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1) .attr(BypassBurnDamageReductionAttr), new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3) - .attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) })) - .attr(PreUseInterruptAttr, (user, target, move) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => !!user.turnData.attacksReceived.find(r => r.damage)) + .attr(MessageHeaderAttr, (user) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) })) + .attr(PreUseInterruptAttr, (user) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => user.turnData.attacksReceived.some(r => r.damage > 0)) .punchingMove(), new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1) @@ -10426,7 +10412,8 @@ export function initMoves() { new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6) .target(MoveTarget.ALL_NEAR_ENEMIES), new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6) - .attr(CelebrateAttr), + // NB: This needs a lambda function as the user will not be logged in by the time the moves are initialized + .attr(MessageAttr, () => i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username })), new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6) .ignoresSubstitute() .target(MoveTarget.NEAR_ALLY), @@ -10601,7 +10588,12 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .reflectable(), new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7) - .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), + .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false) + .attr(MessageAttr, (user) => + i18next.t("battlerTags:laserFocusOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(user), + }), + ), new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute()