[Bug] Fix anger point procing on every hit if first hit in multi hit was a crit

https://github.com/pagefaultgames/pokerogue/pull/6067

* Fix anger point always procing on multi-hit

when first strike was a crit

* Fix comment spacing

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Rename PostDefendCritStatStageChangeAbAttr

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Sirz Benjie 2025-07-07 19:38:18 -06:00 committed by GitHub
parent e82c788585
commit 115d63d0c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 117 additions and 22 deletions

View File

@ -1194,7 +1194,16 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
} }
} }
export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr { /**
* Set stat stages when the user gets hit by a critical hit
*
* @privateremarks
* It is the responsibility of the caller to ensure that this ability attribute is only applied
* when the user has been hit by a critical hit; such an event is not checked here.
*
* @sealed
*/
export class PostReceiveCritStatStageChangeAbAttr extends AbAttr {
private stat: BattleStat; private stat: BattleStat;
private stages: number; private stages: number;
@ -1216,12 +1225,6 @@ export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr {
); );
} }
} }
override getCondition(): AbAttrCondition {
return (pokemon: Pokemon) =>
pokemon.turnData.attacksReceived.length !== 0 &&
pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1].critical;
}
} }
export class PostDefendContactDamageAbAttr extends PostDefendAbAttr { export class PostDefendContactDamageAbAttr extends PostDefendAbAttr {
@ -6417,7 +6420,7 @@ const AbilityAttrs = Object.freeze({
PostDefendContactApplyStatusEffectAbAttr, PostDefendContactApplyStatusEffectAbAttr,
EffectSporeAbAttr, EffectSporeAbAttr,
PostDefendContactApplyTagChanceAbAttr, PostDefendContactApplyTagChanceAbAttr,
PostDefendCritStatStageChangeAbAttr, PostReceiveCritStatStageChangeAbAttr,
PostDefendContactDamageAbAttr, PostDefendContactDamageAbAttr,
PostDefendPerishSongAbAttr, PostDefendPerishSongAbAttr,
PostDefendWeatherChangeAbAttr, PostDefendWeatherChangeAbAttr,
@ -6886,7 +6889,7 @@ export function initAbilities() {
new Ability(AbilityId.GLUTTONY, 4) new Ability(AbilityId.GLUTTONY, 4)
.attr(ReduceBerryUseThresholdAbAttr), .attr(ReduceBerryUseThresholdAbAttr),
new Ability(AbilityId.ANGER_POINT, 4) new Ability(AbilityId.ANGER_POINT, 4)
.attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), .attr(PostReceiveCritStatStageChangeAbAttr, Stat.ATK, 12),
new Ability(AbilityId.UNBURDEN, 4) new Ability(AbilityId.UNBURDEN, 4)
.attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN) .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN)
.bypassFaint() // Allows reviver seed to activate Unburden .bypassFaint() // Allows reviver seed to activate Unburden

View File

@ -432,9 +432,15 @@ export class MoveEffectPhase extends PokemonPhase {
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move * @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param hitResult - The {@linkcode HitResult} of the attempted move * @param hitResult - The {@linkcode HitResult} of the attempted move
* @param wasCritical - `true` if the move was a critical hit
*/ */
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void { protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult, wasCritical = false): void {
applyAbAttrs("PostDefendAbAttr", { pokemon: target, opponent: user, move: this.move, hitResult }); const params = { pokemon: target, opponent: user, move: this.move, hitResult };
applyAbAttrs("PostDefendAbAttr", params);
if (wasCritical) {
applyAbAttrs("PostReceiveCritStatStageChangeAbAttr", params);
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT); target.lapseTags(BattlerTagLapseType.AFTER_HIT);
} }
@ -788,12 +794,12 @@ export class MoveEffectPhase extends PokemonPhase {
this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target); this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target);
const hitResult = this.applyMove(user, target, effectiveness); const [hitResult, wasCritical] = this.applyMove(user, target, effectiveness);
// Apply effects to the user (always) and the target (if not blocked by substitute). // Apply effects to the user (always) and the target (if not blocked by substitute).
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true); this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true);
if (!this.move.hitsSubstitute(user, target)) { if (!this.move.hitsSubstitute(user, target)) {
this.applyOnTargetEffects(user, target, hitResult, firstTarget); this.applyOnTargetEffects(user, target, hitResult, firstTarget, wasCritical);
} }
if (this.lastHit) { if (this.lastHit) {
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
@ -813,8 +819,9 @@ export class MoveEffectPhase extends PokemonPhase {
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - The {@linkcode Pokemon} targeted by the move * @param target - The {@linkcode Pokemon} targeted by the move
* @param effectiveness - The effectiveness of the move against the target * @param effectiveness - The effectiveness of the move against the target
* @returns The {@linkcode HitResult} of the move against the target and a boolean indicating whether the target was crit
*/ */
protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): HitResult { protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] {
const isCritical = target.getCriticalHitResult(user, this.move); const isCritical = target.getCriticalHitResult(user, this.move);
/* /*
@ -845,7 +852,7 @@ export class MoveEffectPhase extends PokemonPhase {
const isOneHitKo = result === HitResult.ONE_HIT_KO; const isOneHitKo = result === HitResult.ONE_HIT_KO;
if (!dmg) { if (!dmg) {
return result; return [result, false];
} }
target.lapseTags(BattlerTagLapseType.HIT); target.lapseTags(BattlerTagLapseType.HIT);
@ -873,7 +880,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
if (damage <= 0) { if (damage <= 0) {
return result; return [result, isCritical];
} }
if (user.isPlayer()) { if (user.isPlayer()) {
@ -902,7 +909,7 @@ export class MoveEffectPhase extends PokemonPhase {
globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage)); globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage));
} }
return result; return [result, isCritical];
} }
/** /**
@ -956,17 +963,17 @@ export class MoveEffectPhase extends PokemonPhase {
* @param target - The {@linkcode Pokemon} struck by the move * @param target - The {@linkcode Pokemon} struck by the move
* @param effectiveness - The effectiveness of the move against the target * @param effectiveness - The effectiveness of the move against the target
*/ */
protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): HitResult { protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] {
const moveCategory = user.getMoveCategory(target, this.move); const moveCategory = user.getMoveCategory(target, this.move);
if (moveCategory === MoveCategory.STATUS) { if (moveCategory === MoveCategory.STATUS) {
return HitResult.STATUS; return [HitResult.STATUS, false];
} }
const result = this.applyMoveDamage(user, target, effectiveness); const result = this.applyMoveDamage(user, target, effectiveness);
if (user.turnData.hitsLeft === 1 || target.isFainted()) { if (user.turnData.hitsLeft === 1 || target.isFainted()) {
this.queueHitResultMessage(result); this.queueHitResultMessage(result[0]);
} }
if (target.isFainted()) { if (target.isFainted()) {
@ -983,8 +990,15 @@ export class MoveEffectPhase extends PokemonPhase {
* @param target - The {@linkcode Pokemon} targeted by the move * @param target - The {@linkcode Pokemon} targeted by the move
* @param hitResult - The {@linkcode HitResult} obtained from applying the move * @param hitResult - The {@linkcode HitResult} obtained from applying the move
* @param firstTarget - `true` if the target is the first Pokemon hit by the attack * @param firstTarget - `true` if the target is the first Pokemon hit by the attack
* @param wasCritical - `true` if the move was a critical hit
*/ */
protected applyOnTargetEffects(user: Pokemon, target: Pokemon, hitResult: HitResult, firstTarget: boolean): void { protected applyOnTargetEffects(
user: Pokemon,
target: Pokemon,
hitResult: HitResult,
firstTarget: boolean,
wasCritical = false,
): void {
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
const dealsDamage = [ const dealsDamage = [
HitResult.EFFECTIVE, HitResult.EFFECTIVE,
@ -995,7 +1009,7 @@ export class MoveEffectPhase extends PokemonPhase {
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false); this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false);
this.applyHeldItemFlinchCheck(user, target, dealsDamage); this.applyHeldItemFlinchCheck(user, target, dealsDamage);
this.applyOnGetHitAbEffects(user, target, hitResult); this.applyOnGetHitAbEffects(user, target, hitResult, wasCritical);
applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult }); applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult });
// We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens // We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens

View File

@ -0,0 +1,78 @@
import { PostReceiveCritStatStageChangeAbAttr } from "#app/data/abilities/ability";
import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Ability - Anger Point", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.ability(AbilityId.BALL_FETCH)
.battleStyle("single")
.criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH)
.enemyLevel(100);
});
it("should set the user's attack stage to +6 when hit by a critical hit", async () => {
game.override.enemyAbility(AbilityId.ANGER_POINT).moveset(MoveId.FALSE_SWIPE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const enemy = game.scene.getEnemyPokemon()!;
// minimize the enemy's attack stage to ensure it is always set to +6
enemy.setStatStage(Stat.ATK, -6);
vi.spyOn(enemy, "getCriticalHitResult").mockReturnValueOnce(true);
game.move.select(MoveId.FALSE_SWIPE);
await game.phaseInterceptor.to("BerryPhase");
expect(enemy.getStatStage(Stat.ATK)).toBe(6);
});
it("should only proc once when a multi-hit move crits on the first hit", async () => {
game.override
.moveset(MoveId.BULLET_SEED)
.enemyLevel(50)
.enemyAbility(AbilityId.ANGER_POINT)
.ability(AbilityId.SKILL_LINK);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "getCriticalHitResult").mockReturnValueOnce(true);
const angerPointSpy = vi.spyOn(PostReceiveCritStatStageChangeAbAttr.prototype, "apply");
game.move.select(MoveId.BULLET_SEED);
await game.phaseInterceptor.to("BerryPhase");
expect(angerPointSpy).toHaveBeenCalledTimes(1);
});
it("should set a contrary user's attack stage to -6 when hit by a critical hit", async () => {
game.override
.enemyAbility(AbilityId.ANGER_POINT)
.enemyPassiveAbility(AbilityId.CONTRARY)
.enemyHasPassiveAbility(true)
.moveset(MoveId.FALSE_SWIPE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "getCriticalHitResult").mockReturnValueOnce(true);
enemy.setStatStage(Stat.ATK, 6);
game.move.select(MoveId.FALSE_SWIPE);
await game.phaseInterceptor.to("BerryPhase");
expect(enemy.getStatStage(Stat.ATK)).toBe(-6);
});
});