From 1474f8cf147f947f2d30ca92d8b0c727cf463199 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 2 Nov 2024 10:05:33 -0700 Subject: [PATCH] [Refactor] Add `options` param interface for `MoveEffectAttr` (#4710) * Optional parameter interfaces for `MoveEffectAttr` and `StatStageChangeAttr` * Update docs + Diamond Storm typo * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Make move effect trigger specification optional --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/move.ts | 247 ++++++++++++++++++---------- src/test/moves/secret_power.test.ts | 21 ++- 2 files changed, 171 insertions(+), 97 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index cbafb4e66a3..a8a9b6ab2f7 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1049,31 +1049,80 @@ export enum MoveEffectTrigger { POST_TARGET, } +interface MoveEffectAttrOptions { + /** + * Defines when this effect should trigger in the move's effect order + * @see {@linkcode MoveEffectPhase} + */ + trigger?: MoveEffectTrigger; + /** Should this effect only apply on the first hit? */ + firstHitOnly?: boolean; + /** Should this effect only apply on the last hit? */ + lastHitOnly?: boolean; + /** Should this effect only apply on the first target hit? */ + firstTargetOnly?: boolean; + /** Overrides the secondary effect chance for this attr if set. */ + effectChanceOverride?: number; +} + /** Base class defining all Move Effect Attributes * @extends MoveAttr * @see {@linkcode apply} */ export class MoveEffectAttr extends MoveAttr { - /** Defines when this effect should trigger in the move's effect order - * @see {@linkcode phases.MoveEffectPhase.start} + /** + * A container for this attribute's optional parameters + * @see {@linkcode MoveEffectAttrOptions} for supported params. */ - public trigger: MoveEffectTrigger; - /** Should this effect only apply on the first hit? */ - public firstHitOnly: boolean; - /** Should this effect only apply on the last hit? */ - public lastHitOnly: boolean; - /** Should this effect only apply on the first target hit? */ - public firstTargetOnly: boolean; - /** Overrides the secondary effect chance for this attr if set. */ - public effectChanceOverride?: number; + protected options?: MoveEffectAttrOptions; - constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) { + constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) { super(selfTarget); - this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY; - this.firstHitOnly = firstHitOnly; - this.lastHitOnly = lastHitOnly; - this.firstTargetOnly = firstTargetOnly; - this.effectChanceOverride = effectChanceOverride; + this.options = options; + } + + /** + * Defines when this effect should trigger in the move's effect order. + * @default MoveEffectTrigger.POST_APPLY + * @see {@linkcode MoveEffectTrigger} + */ + public get trigger () { + return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY; + } + + /** + * `true` if this effect should only trigger on the first hit of + * multi-hit moves. + * @default false + */ + public get firstHitOnly () { + return this.options?.firstHitOnly ?? false; + } + + /** + * `true` if this effect should only trigger on the last hit of + * multi-hit moves. + * @default false + */ + public get lastHitOnly () { + return this.options?.lastHitOnly ?? false; + } + + /** + * `true` if this effect should apply only upon hitting a target + * for the first time when targeting multiple {@linkcode Pokemon}. + * @default false + */ + public get firstTargetOnly () { + return this.options?.firstTargetOnly ?? false; + } + + /** + * If defined, overrides the move's base chance for this + * secondary effect to trigger. + */ + public get effectChanceOverride () { + return this.options?.effectChanceOverride; } /** @@ -1398,7 +1447,7 @@ export class RecoilAttr extends MoveEffectAttr { private unblockable: boolean; constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY, false, true); + super(true, { lastHitOnly: true }); this.useHp = useHp; this.damageRatio = damageRatio; @@ -1456,7 +1505,7 @@ export class RecoilAttr extends MoveEffectAttr { **/ export class SacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); } /** @@ -1489,7 +1538,7 @@ export class SacrificialAttr extends MoveEffectAttr { **/ export class SacrificialAttrOnHit extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); } /** @@ -1528,7 +1577,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr { */ export class HalfSacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); } /** @@ -1932,7 +1981,7 @@ export class HitHealAttr extends MoveEffectAttr { private healStat: EffectiveStat | null; constructor(healRatio?: number | null, healStat?: EffectiveStat) { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); this.healRatio = healRatio ?? 0.5; this.healStat = healStat ?? null; @@ -2141,7 +2190,7 @@ export class StatusEffectAttr extends MoveEffectAttr { public overrideStatus: boolean = false; constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { - super(selfTarget, MoveEffectTrigger.HIT); + super(selfTarget, { trigger: MoveEffectTrigger.HIT }); this.effect = effect; this.turnsRemaining = turnsRemaining; @@ -2214,7 +2263,7 @@ export class MultiStatusEffectAttr extends StatusEffectAttr { export class PsychoShiftEffectAttr extends MoveEffectAttr { constructor() { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -2251,7 +2300,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { private chance: number; constructor(chance: number) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.chance = chance; } @@ -2312,7 +2361,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { private berriesOnly: boolean; constructor(berriesOnly: boolean) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.berriesOnly = berriesOnly; } @@ -2386,7 +2435,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr { protected chosenBerry: BerryModifier | undefined; constructor() { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); } /** * Causes the target to eat a berry. @@ -2489,7 +2538,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { * @param ...effects - List of status effects to cure */ constructor(selfTarget: boolean, ...effects: StatusEffect[]) { - super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true); + super(selfTarget, { lastHitOnly: true }); this.effects = effects; } @@ -2819,35 +2868,67 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { } } +/** + * Set of optional parameters that may be applied to stat stage changing effects + * @extends MoveEffectAttrOptions + * @see {@linkcode StatStageChangeAttr} + */ +interface StatStageChangeAttrOptions extends MoveEffectAttrOptions { + /** If defined, needs to be met in order for the stat change to apply */ + condition?: MoveConditionFunc, + /** `true` to display a message */ + showMessage?: boolean +} + /** * Attribute used for moves that change stat stages * * @param stats {@linkcode BattleStat} Array of stat(s) to change * @param stages How many stages to change the stat(s) by, [-6, 6] * @param selfTarget `true` if the move is self-targetting - * @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes - * @param showMessage `true` to display a message; default `true` - * @param firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false` - * @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT} - * @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false` - * @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false` - * @param effectChanceOverride Will override the move's normal secondary effect chance if specified + * @param options {@linkcode StatStageChangeAttrOptions} Container for any optional parameters for this attribute. * * @extends MoveEffectAttr * @see {@linkcode apply} */ export class StatStageChangeAttr extends MoveEffectAttr { public stats: BattleStat[]; - public stages: integer; - private condition?: MoveConditionFunc | null; - private showMessage: boolean; + public stages: number; + /** + * Container for optional parameters to this attribute. + * @see {@linkcode StatStageChangeAttrOptions} for available optional params + */ + protected override options?: StatStageChangeAttrOptions; - constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false, effectChanceOverride?: number) { - super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride); + constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, options?: StatStageChangeAttrOptions) { + super(selfTarget, options); this.stats = stats; this.stages = stages; - this.condition = condition; - this.showMessage = showMessage; + this.options = options; + } + + /** + * The condition required for the stat stage change to apply. + * Defaults to `null` (i.e. no condition required). + */ + private get condition () { + return this.options?.condition ?? null; + } + + /** + * `true` to display a message for the stat change. + * @default true + */ + private get showMessage () { + return this.options?.showMessage ?? true; + } + + /** + * Indicates when the stat change should trigger + * @default MoveEffectTrigger.HIT + */ + public override get trigger () { + return this.options?.trigger ?? MoveEffectTrigger.HIT; } /** @@ -2932,20 +3013,6 @@ export class SecretPowerAttr extends MoveEffectAttr { super(false); } - /** - * Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance - * @returns `true` if the move's secondary effect should apply - */ - override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - this.effectChanceOverride = move.chance; - const moveChance = this.getMoveChance(user, target, move, this.selfTarget); - if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { - return true; - } else { - return false; - } - } - /** * Used to apply the secondary effect to the target Pokemon * @returns `true` if a secondary effect is successfully applied @@ -2962,8 +3029,6 @@ export class SecretPowerAttr extends MoveEffectAttr { const biome = user.scene.arena.biomeType; secondaryEffect = this.determineBiomeEffect(biome); } - // effectChanceOverride used in the application of the actual secondary effect - secondaryEffect.effectChanceOverride = 100; return secondaryEffect.apply(user, target, move, []); } @@ -3139,7 +3204,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr { private messageCallback: ((user: Pokemon) => void) | undefined; constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) { - super(stat, levels, true, null, true); + super(stat, levels, true); this.cutRatio = cutRatio; this.messageCallback = messageCallback; @@ -4889,7 +4954,7 @@ export class BypassRedirectAttr extends MoveAttr { export class FrenzyAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT, false, true); + super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true }); } canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { @@ -4962,7 +5027,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { private failOnOverlap: boolean; constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) { - super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly); + super(selfTarget, { lastHitOnly: lastHitOnly }); this.tagType = tagType; this.turnCountMin = turnCountMin; @@ -5397,7 +5462,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.tagType = tagType; this.turnCount = turnCount!; // TODO: is the bang correct? @@ -5435,7 +5500,7 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.tagTypes = tagTypes; this.selfSideTarget = selfSideTarget; @@ -5501,7 +5566,7 @@ export class RemoveArenaTrapAttr extends MoveEffectAttr { private targetBothSides: boolean; constructor(targetBothSides: boolean = false) { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); this.targetBothSides = targetBothSides; } @@ -5537,7 +5602,7 @@ export class RemoveScreensAttr extends MoveEffectAttr { private targetBothSides: boolean; constructor(targetBothSides: boolean = false) { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); this.targetBothSides = targetBothSides; } @@ -5575,7 +5640,7 @@ export class SwapArenaTagsAttr extends MoveEffectAttr { constructor(SwapTags: ArenaTagType[]) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.SwapTags = SwapTags; } @@ -5701,7 +5766,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { private selfSwitch: boolean = false, private switchType: SwitchType = SwitchType.SWITCH ) { - super(false, MoveEffectTrigger.POST_APPLY, false, true); + super(false, { lastHitOnly: true }); } isBatonPass() { @@ -5856,7 +5921,7 @@ export class RemoveTypeAttr extends MoveEffectAttr { private messageCallback: ((user: Pokemon) => void) | undefined; constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); this.removedType = removedType; this.messageCallback = messageCallback; @@ -6032,7 +6097,7 @@ export class ChangeTypeAttr extends MoveEffectAttr { private type: Type; constructor(type: Type) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.type = type; } @@ -6055,7 +6120,7 @@ export class AddTypeAttr extends MoveEffectAttr { private type: Type; constructor(type: Type) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.type = type; } @@ -6582,7 +6647,7 @@ export class AbilityChangeAttr extends MoveEffectAttr { public ability: Abilities; constructor(ability: Abilities, selfTarget?: boolean) { - super(selfTarget, MoveEffectTrigger.HIT); + super(selfTarget, { trigger: MoveEffectTrigger.HIT }); this.ability = ability; } @@ -6611,7 +6676,7 @@ export class AbilityCopyAttr extends MoveEffectAttr { public copyToPartner: boolean; constructor(copyToPartner: boolean = false) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.copyToPartner = copyToPartner; } @@ -6650,7 +6715,7 @@ export class AbilityGiveAttr extends MoveEffectAttr { public copyToPartner: boolean; constructor() { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -6962,7 +7027,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr { export class MoneyAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT, true); + super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true }); } apply(user: Pokemon, target: Pokemon, move: Move): boolean { @@ -6979,7 +7044,7 @@ export class MoneyAttr extends MoveEffectAttr { */ export class DestinyBondAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); } /** @@ -7029,7 +7094,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr { public effect: StatusEffect; constructor(effect: StatusEffect) { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); this.effect = effect; } @@ -9087,7 +9152,7 @@ export function initMoves() { // If any fielded pokémon is grass-type and grounded. return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); }) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }), new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) .target(MoveTarget.ENEMY_SIDE), @@ -9124,7 +9189,7 @@ export function initMoves() { .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY }) .attr(ForceSwitchOutAttr, true) .soundBased(), new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) @@ -9139,7 +9204,7 @@ export function initMoves() { .condition(failIfLastCondition), new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) - .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag) }), new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) .attr(TerrainChangeAttr, TerrainType.GRASSY) .target(MoveTarget.BOTH_SIDES), @@ -9171,7 +9236,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .soundBased(), new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) - .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, undefined, undefined, undefined, undefined, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true }) .makesContact(false) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6) @@ -9197,7 +9262,7 @@ export function initMoves() { new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC }) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) .ignoresSubstitute() @@ -9208,7 +9273,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .ignoresVirtual(), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), @@ -9428,7 +9493,7 @@ export function initMoves() { new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), @@ -9485,7 +9550,7 @@ export function initMoves() { .ballBombMove() .makesContact(false), new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7), @@ -9599,7 +9664,7 @@ export function initMoves() { .makesContact(false) .ignoresVirtual(), new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, undefined, undefined, undefined, undefined, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES) .edgeCase() // I assume it needs clanging scales and Kommo-O @@ -9837,8 +9902,8 @@ export function initMoves() { .attr(ClearTerrainAttr) .condition((user, target, move) => !!user.scene.arena.terrain), new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) - .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true }) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true }) .attr(MultiHitAttr) .makesContact(false), new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) @@ -9972,7 +10037,7 @@ export function initMoves() { new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) .makesContact(false) .attr(HighCritAttr) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, false, { effectChanceOverride: 50 }) .attr(FlinchAttr), new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) .attr(StatusEffectAttr, StatusEffect.BURN) @@ -10108,7 +10173,7 @@ export function initMoves() { .attr(TeraMoveCategoryAttr) .attr(TeraBlastTypeAttr) .attr(TeraBlastPowerAttr) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) }) .partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */ new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) .attr(ProtectAttr, BattlerTagType.SILK_TRAP) @@ -10192,7 +10257,7 @@ export function initMoves() { .attr(RemoveScreensAttr), new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(MoneyAttr) - .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, { firstTargetOnly: true }) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1) @@ -10215,7 +10280,7 @@ export function initMoves() { .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) .attr(ChillyReceptionAttr, true), new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true) .attr(RemoveArenaTrapAttr, true) .attr(RemoveAllSubstitutesAttr), new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9) diff --git a/src/test/moves/secret_power.test.ts b/src/test/moves/secret_power.test.ts index ff0b5ae8c24..09fe5faa50b 100644 --- a/src/test/moves/secret_power.test.ts +++ b/src/test/moves/secret_power.test.ts @@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Stat } from "#enums/stat"; -import { allMoves, SecretPowerAttr } from "#app/data/move"; +import { allMoves } from "#app/data/move"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; @@ -11,6 +11,7 @@ import { StatusEffect } from "#enums/status-effect"; import { BattlerIndex } from "#app/battle"; import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagSide } from "#app/data/arena-tag"; +import { allAbilities, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; describe("Moves - Secret Power", () => { let phaserGame: Phaser.Game; @@ -60,30 +61,38 @@ describe("Moves - Secret Power", () => { expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); }); - it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect", + it("Secret Power's effect chance is doubled by Serene Grace, but not by the 'rainbow' effect from Fire/Water Pledge", async () => { game.override .moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ]) + .ability(Abilities.SERENE_GRACE) .enemyMoveset([ Moves.SPLASH ]) .battleType("double"); await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]); - const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0]; - vi.spyOn(secretPowerAttr, "getMoveChance"); + const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0]; + vi.spyOn(sereneGraceAttr, "apply"); game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY); game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined(); + let rainbowEffect = game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER); + expect(rainbowEffect).toBeDefined(); + + rainbowEffect = rainbowEffect!; + vi.spyOn(rainbowEffect, "apply"); game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY); game.move.select(Moves.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase", false); - expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30); + expect(sereneGraceAttr.apply).toHaveBeenCalledOnce(); + expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true); + + expect(rainbowEffect.apply).toHaveBeenCalledTimes(0); } ); });