From e1aded9504ab1642fb1135cb2e4d637b737315de Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:03:32 -0500 Subject: [PATCH] [Bug] Fix Parental Bond reducing damage of spread moves on 2nd pokemon https://github.com/pagefaultgames/pokerogue/pull/6743 * Fix Pollen Puff interaction with Parental Bond --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/abilities/ability.ts | 59 +++++++++++----------------- src/data/moves/move.ts | 50 +++++++++++++---------- src/field/pokemon.ts | 12 +----- src/phases/move-effect-phase.ts | 2 +- test/abilities/parental-bond.test.ts | 20 ++++++++++ test/items/multi-lens.test.ts | 54 +++++++------------------ 6 files changed, 90 insertions(+), 107 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index cd18bbcfb9c..21a00e53aed 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1833,13 +1833,13 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr { } /** - * Parameters for abilities that modify the hit count and damage of a move + * Parameters for abilities that modify the hit count of a move. */ export interface AddSecondStrikeAbAttrParams extends Omit { - /** Holder for the number of hits. May be modified by ability application */ - hitCount?: NumberHolder; - /** Holder for the damage multiplier _of the current hit_ */ - multiplier?: NumberHolder; + /** Holder for the number of hits. Modified by ability application */ + hitCount: NumberHolder; + /** The Pokemon on the other side of this interaction */ + opponent: Pokemon | undefined; } /** @@ -1847,35 +1847,12 @@ export interface AddSecondStrikeAbAttrParams extends Omit (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5) + .attr(MoveDamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5) .build(), new AbBuilder(AbilityId.FILTER, 4) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75) @@ -7636,7 +7615,15 @@ export function initAbilities() { .attr(MoveTypeChangeAbAttr, PokemonType.FLYING, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL) .build(), new AbBuilder(AbilityId.PARENTAL_BOND, 6) - .attr(AddSecondStrikeAbAttr, 0.25) + .attr(AddSecondStrikeAbAttr) + // Only multiply damage on the last strike of multi-strike moves + .attr(MoveDamageBoostAbAttr, 0.25, (user, target, move) => ( + !!user + && user.turnData.hitCount > 1 // move was originally multi hit + && user.turnData.hitsLeft === 1 // move is on its final strike + && move.canBeMultiStrikeEnhanced(user, true, target) + ) + ) .build(), new AbBuilder(AbilityId.DARK_AURA, 6) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonDarkAura", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 6e29f6b0ac9..382ee0ce68e 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -100,7 +100,6 @@ import i18next from "i18next"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { inSpeedOrder } from "#utils/speed-order-generator"; import { canSpeciesTera, willTerastallize } from "#utils/pokemon-utils"; -import type { ReadonlyGenericUint8Array } from "#types/typed-arrays"; import { MovePriorityInBracket } from "#enums/move-priority-in-bracket"; /** @@ -1117,20 +1116,34 @@ export abstract class Move implements Localizable { } /** - * Returns `true` if this move can be given additional strikes - * by enhancing effects. + * Check whether this Move can be given additional strikes from enhancing effects. * Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond} - * and {@linkcode PokemonMultiHitModifier | Multi-Lens}. - * @param user The {@linkcode Pokemon} using the move - * @param restrictSpread `true` if the enhancing effect - * should not affect multi-target moves (default `false`) + * and {@linkcode PokemonMultiHitModifier | Multi Lens}. + * @param user - The {@linkcode Pokemon} using the move + * @param restrictSpread - Whether the enhancing effect should ignore multi-target moves; default `false` + * @returns Whether this Move can be given additional strikes. */ - canBeMultiStrikeEnhanced(user: Pokemon, restrictSpread: boolean = false): boolean { + // TODO: Remove target parameter used solely to circumvent Pollen Puff shenanigans - the entire move needs to be fixed anyhow + public canBeMultiStrikeEnhanced(user: Pokemon, restrictSpread: boolean = false, target?: Pokemon | null): boolean { // Multi-strike enhancers... - // ...cannot enhance moves that hit multiple targets + // ...cannot enhance charging or 2-turn moves + if (this.isChargingMove()) { + return false; + } + + // ...cannot enhance moves hitting multiple targets unless specified const { targets, multiple } = getMoveTargets(user, this.id); - const isMultiTarget = multiple && targets.length > 1; + if (restrictSpread && multiple && targets.length > 1) { + return false; + }; + + // ...cannot enhance status moves, including ally-targeting Pollen Puff + if ( + this.category === MoveCategory.STATUS + || (target != null && user.getMoveCategory(target, this) === MoveCategory.STATUS)) { + return false; + } // ...cannot enhance multi-hit or sacrificial moves const exceptAttrs: MoveAttrString[] = [ @@ -1138,6 +1151,9 @@ export abstract class Move implements Localizable { "SacrificialAttr", "SacrificialAttrOnHit" ]; + if (exceptAttrs.some(attr => this.hasAttr(attr))) { + return false; + } // ...and cannot enhance these specific moves const exceptMoves: MoveId[] = [ @@ -1147,17 +1163,11 @@ export abstract class Move implements Localizable { MoveId.ICE_BALL, MoveId.ENDEAVOR ]; + if (exceptMoves.includes(this.id)) { + return false; + } - // ...and cannot enhance Pollen Puff when targeting an ally. - const ally = user.getAlly(); - const exceptPollenPuffAlly: boolean = this.id === MoveId.POLLEN_PUFF && ally != null && targets.includes(ally.getBattlerIndex()) - - return (!restrictSpread || !isMultiTarget) - && !this.isChargingMove() - && !exceptAttrs.some(attr => this.hasAttr(attr)) - && !exceptMoves.some(id => this.id === id) - && !exceptPollenPuffAlly - && this.category !== MoveCategory.STATUS; + return true; } } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 1d58f7de883..9f6b3b099e9 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3672,15 +3672,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { multiStrikeEnhancementMultiplier, ); - if (!ignoreSourceAbility) { - applyAbAttrs("AddSecondStrikeAbAttr", { - pokemon: source, - move, - simulated, - multiplier: multiStrikeEnhancementMultiplier, - }); - } - /** Doubles damage if this Pokemon's last move was Glaive Rush */ const glaiveRushMultiplier = new NumberHolder(1); if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) { @@ -3769,9 +3760,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * mistyTerrainMultiplier, ); - /** Doubles damage if the attacker has Tinted Lens and is using a resisted move */ if (!ignoreSourceAbility) { - applyAbAttrs("DamageBoostAbAttr", { + applyAbAttrs("MoveDamageBoostAbAttr", { pokemon: source, opponent: this, move, diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 476f0de4a36..3209298a265 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -276,7 +276,7 @@ export class MoveEffectPhase extends PokemonPhase { // Assume single target for multi hit applyMoveAttrs("MultiHitAttr", user, this.getFirstTarget() ?? null, move, hitCount); // If Parental Bond is applicable, add another hit - applyAbAttrs("AddSecondStrikeAbAttr", { pokemon: user, move, hitCount }); + applyAbAttrs("AddSecondStrikeAbAttr", { pokemon: user, move, hitCount, opponent: this.getFirstTarget() }); // If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount); // Set the user's relevant turnData fields to reflect the final hit count diff --git a/test/abilities/parental-bond.test.ts b/test/abilities/parental-bond.test.ts index a72fc82260f..95f0e8d4159 100644 --- a/test/abilities/parental-bond.test.ts +++ b/test/abilities/parental-bond.test.ts @@ -384,4 +384,24 @@ describe("Abilities - Parental Bond", () => { // TODO: Update hit count to 1 once Future Sight is fixed to not activate abilities if user is off the field expect(enemyPokemon.damageAndUpdate).toHaveBeenCalledTimes(2); }); + + it("should not reduce damage against the remaining target if the first one faints", async () => { + game.override.battleStyle("double").enemySpecies(SpeciesId.MAGIKARP); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const feebas = game.field.getPlayerPokemon(); + const [karp1, karp2] = game.scene.getEnemyField(); + + // Mock base damage for both mons for consistent results + vi.spyOn(karp1, "getBaseDamage").mockReturnValue(100); + vi.spyOn(karp2, "getBaseDamage").mockReturnValue(100); + karp1.hp = 1; + + game.move.use(MoveId.HYPER_VOICE); + await game.toEndOfTurn(); + + expect(karp1).toHaveFainted(); + expect(feebas).not.toHaveAbilityApplied(AbilityId.PARENTAL_BOND); + expect(karp2).toHaveTakenDamage(100); + }); }); diff --git a/test/items/multi-lens.test.ts b/test/items/multi-lens.test.ts index 3686aff0fcf..bdf93a4ae12 100644 --- a/test/items/multi-lens.test.ts +++ b/test/items/multi-lens.test.ts @@ -26,6 +26,7 @@ describe("Items - Multi Lens", () => { game.override .moveset([MoveId.TACKLE, MoveId.TRAILBLAZE, MoveId.TACHYON_CUTTER, MoveId.FUTURE_SIGHT]) .ability(AbilityId.BALL_FETCH) + .passiveAbility(AbilityId.NO_GUARD) .startingHeldItems([{ name: "MULTI_LENS" }]) .battleStyle("single") .criticalHits(false) @@ -135,61 +136,36 @@ describe("Items - Multi Lens", () => { expect(damageResults[1]).toBe(Math.floor(playerPokemon.level * 0.25)); }); - it("should result in correct damage for hp% attacks with 1 lens", async () => { + it.each([1, 2])("should result in original damage for HP-cutting attacks with %d lenses", async lensCount => { game.override - .startingHeldItems([{ name: "MULTI_LENS", count: 1 }]) - .moveset(MoveId.SUPER_FANG) - .ability(AbilityId.COMPOUND_EYES) + .startingHeldItems([{ name: "MULTI_LENS", count: lensCount }]) .enemyLevel(1000) .enemySpecies(SpeciesId.BLISSEY); // allows for unrealistically high levels of accuracy + await game.classicMode.startBattle([SpeciesId.FEEBAS]); - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + const blissey = game.field.getEnemyPokemon(); - const enemyPokemon = game.field.getEnemyPokemon(); + game.move.use(MoveId.SUPER_FANG); + await game.toEndOfTurn(); - game.move.select(MoveId.SUPER_FANG); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to("MoveEndPhase"); - expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5); + expect(blissey.getHpRatio()).toBeCloseTo(0.5, 5); }); - it("should result in correct damage for hp% attacks with 2 lenses", async () => { + it("should result in original damage for HP-cutting attacks with 2 lenses + Parental Bond", async () => { game.override .startingHeldItems([{ name: "MULTI_LENS", count: 2 }]) - .moveset(MoveId.SUPER_FANG) - .ability(AbilityId.COMPOUND_EYES) - .enemyMoveset(MoveId.SPLASH) - .enemyLevel(1000) - .enemySpecies(SpeciesId.BLISSEY); // allows for unrealistically high levels of accuracy - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const enemyPokemon = game.field.getEnemyPokemon(); - - game.move.select(MoveId.SUPER_FANG); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to("MoveEndPhase"); - expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5); - }); - - it("should result in correct damage for hp% attacks with 2 lenses + Parental Bond", async () => { - game.override - .startingHeldItems([{ name: "MULTI_LENS", count: 2 }]) - .moveset(MoveId.SUPER_FANG) .ability(AbilityId.PARENTAL_BOND) - .passiveAbility(AbilityId.COMPOUND_EYES) - .enemyMoveset(MoveId.SPLASH) .enemyLevel(1000) .enemySpecies(SpeciesId.BLISSEY); // allows for unrealistically high levels of accuracy - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemyPokemon = game.field.getEnemyPokemon(); + const blissey = game.field.getEnemyPokemon(); - game.move.select(MoveId.SUPER_FANG); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to("MoveEndPhase"); - expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.25, 5); + game.move.use(MoveId.SUPER_FANG); + await game.toEndOfTurn(); + + expect(blissey.getHpRatio()).toBeCloseTo(0.25, 5); }); it("should not allow Future Sight to hit infinitely many times if the user switches out", async () => {