[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>
This commit is contained in:
Bertie690 2025-11-06 21:03:32 -05:00 committed by GitHub
parent c7b563e498
commit e1aded9504
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 90 additions and 107 deletions

View File

@ -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<AugmentMoveInteractionAbAttrParams, "opponent"> {
/** 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<AugmentMoveInteraction
* Used by {@linkcode MoveId.PARENTAL_BOND | Parental Bond}.
*/
export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
/** The damage multiplier for the second strike, relative to the first */
private readonly damageMultiplier: number;
/**
* @param damageMultiplier - The damage multiplier for the second strike, relative to the first
*/
constructor(damageMultiplier: number) {
super(false);
this.damageMultiplier = damageMultiplier;
override canApply({ pokemon, opponent, move }: AddSecondStrikeAbAttrParams): boolean {
return move.canBeMultiStrikeEnhanced(pokemon, true, opponent);
}
/**
* Return whether the move can be multi-strike enhanced.
*/
override canApply({ pokemon, move }: AddSecondStrikeAbAttrParams): boolean {
return move.canBeMultiStrikeEnhanced(pokemon, true);
}
/**
* Add one to the move's hit count, and, if the pokemon has only one hit left, sets the damage multiplier
* to the damage multiplier of this ability.
*/
override apply({ hitCount, multiplier, pokemon }: AddSecondStrikeAbAttrParams): void {
if (hitCount?.value) {
hitCount.value += 1;
}
if (multiplier?.value && pokemon.turnData.hitsLeft === 1) {
multiplier.value = this.damageMultiplier;
}
override apply({ hitCount }: AddSecondStrikeAbAttrParams): void {
hitCount.value += 1;
}
}
@ -1895,10 +1872,12 @@ export interface PreAttackModifyDamageAbAttrParams extends AugmentMoveInteractio
* @param damageMultiplier the amount to multiply the damage by
* @param condition the condition for this ability to be applied
*/
export class DamageBoostAbAttr extends PreAttackAbAttr {
export class MoveDamageBoostAbAttr extends PreAttackAbAttr {
private readonly damageMultiplier: number;
private readonly condition: PokemonAttackCondition;
// TODO: This should not take a `PokemonAttackCondition` (with nullish parameters)
// as it's effectively offloading nullishness checks to its child attributes
constructor(damageMultiplier: number, condition: PokemonAttackCondition) {
super(false);
this.damageMultiplier = damageMultiplier;
@ -6657,7 +6636,7 @@ const AbilityAttrs = Object.freeze({
MoveTypeChangeAbAttr,
PokemonTypeChangeAbAttr,
AddSecondStrikeAbAttr,
DamageBoostAbAttr,
MoveDamageBoostAbAttr,
MovePowerBoostAbAttr,
MoveTypePowerBoostAbAttr,
LowHpMoveTypePowerBoostAbAttr,
@ -7298,7 +7277,7 @@ export function initAbilities() {
.ignorable()
.build(),
new AbBuilder(AbilityId.TINTED_LENS, 4)
.attr(DamageBoostAbAttr, 2, (user, target, move) => (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) }))

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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

View File

@ -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);
});
});

View File

@ -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 () => {