mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2024-11-29 10:16:14 +00:00
[Bug] Prevent fixed-damage and OHKO moves from being modified by damage-reducing abilities (#2703)
* ReceivedMoveDamageMultiplierAbAttr patch: WIP refactored damage calculation, reordered ReceivedMoveDamageMultiplierAbAttr to avoid issues with fixed damage and OHKO moves, stubbed unit tests for dragon rage (fixed damage) and fissure (OHKO) * ReceivedMoveDamageMultiplierAbAttr patch: commented concerns regarding EnemyDamageBooster/ReducerModifier for others' reference in WIP branch * ReceivedMoveDamageMultiplierAbAttr patch: reordered ReceivedMoveDamageMultiplierAbAttr and EnemyDamageBooster/ReducerModifier to not trigger for fixed damage and OHKO moves, completed relevant tests for dragon rage and fissure * ReceivedMoveDamageMultiplierAbAttr patch: removed newline * ReceivedMoveDamageMultiplierAbAttr patch: in the unit test, extracted hard-coded Dragon Rage damage to a variable * ReceivedMoveDamageMultiplierAbAttr patch: naming consistency * ReceivedMoveDamageMultiplierAbAttr patch: replaced awaiting DamagePhase with TurnEndPhase as the former assumes damage will be done * ReceivedMoveDamageMultiplierAbAttr patch: removed redundant overrides in Fissure tests * ReceivedMoveDamageMultiplierAbAttr patch: tests: refactored crit removal, removed berries, fixed bug associated with Porygon sometimes getting Trace and copying the opponent's ability, which would override the manual ability override * Fixed unit tests * Added a comment and cleaned up an existing one
This commit is contained in:
parent
a9a071bb4d
commit
b1e7ae43a1
@ -1869,9 +1869,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, numTargets, new Utils.IntegerHolder(0), twoStrikeMultiplier);
|
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, numTargets, new Utils.IntegerHolder(0), twoStrikeMultiplier);
|
||||||
|
|
||||||
if (!isTypeImmune) {
|
if (!isTypeImmune) {
|
||||||
damage.value = Math.ceil(
|
const levelMultiplier = (2 * source.level / 5 + 2);
|
||||||
((((2 * source.level / 5 + 2) * power * sourceAtk.value / targetDef.value) / 50) + 2)
|
const randomMultiplier = ((this.scene.randBattleSeedInt(16) + 85) / 100);
|
||||||
* stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * twoStrikeMultiplier.value * ((this.scene.randBattleSeedInt(16) + 85) / 100) * criticalMultiplier.value);
|
damage.value = Math.ceil((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2)
|
||||||
|
* stabMultiplier.value
|
||||||
|
* typeMultiplier.value
|
||||||
|
* arenaAttackTypeMultiplier.value
|
||||||
|
* screenMultiplier.value
|
||||||
|
* twoStrikeMultiplier.value
|
||||||
|
* criticalMultiplier.value
|
||||||
|
* randomMultiplier);
|
||||||
|
|
||||||
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
|
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
|
||||||
if (!move.hasAttr(BypassBurnDamageReductionAttr)) {
|
if (!move.hasAttr(BypassBurnDamageReductionAttr)) {
|
||||||
const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
|
const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
|
||||||
@ -1913,9 +1921,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (!typeMultiplier.value) {
|
if (!typeMultiplier.value) {
|
||||||
result = move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT;
|
result = move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT;
|
||||||
} else {
|
} else {
|
||||||
const oneHitKo = new Utils.BooleanHolder(false);
|
const isOneHitKo = new Utils.BooleanHolder(false);
|
||||||
applyMoveAttrs(OneHitKOAttr, source, this, move, oneHitKo);
|
applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo);
|
||||||
if (oneHitKo.value) {
|
if (isOneHitKo.value) {
|
||||||
result = HitResult.ONE_HIT_KO;
|
result = HitResult.ONE_HIT_KO;
|
||||||
isCritical = false;
|
isCritical = false;
|
||||||
damage.value = this.hp;
|
damage.value = this.hp;
|
||||||
@ -1929,24 +1937,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fixedDamage.value) {
|
const isOneHitKo = result === HitResult.ONE_HIT_KO;
|
||||||
|
|
||||||
|
if (!fixedDamage.value && !isOneHitKo) {
|
||||||
if (!source.isPlayer()) {
|
if (!source.isPlayer()) {
|
||||||
this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage);
|
this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage);
|
||||||
}
|
}
|
||||||
if (!this.isPlayer()) {
|
if (!this.isPlayer()) {
|
||||||
this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage);
|
this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, damage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This attribute may modify damage arbitrarily, so be careful about changing its order of application.
|
||||||
applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage);
|
applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage);
|
||||||
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, damage);
|
|
||||||
|
|
||||||
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef);
|
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef);
|
||||||
|
|
||||||
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
|
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
|
||||||
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
|
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
|
||||||
|
|
||||||
const oneHitKo = result === HitResult.ONE_HIT_KO;
|
|
||||||
if (damage.value) {
|
if (damage.value) {
|
||||||
if (this.getHpRatio() === 1) {
|
if (this.getHpRatio() === 1) {
|
||||||
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, damage);
|
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, damage);
|
||||||
@ -1955,10 +1966,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We explicitly require to ignore the faint phase here, as we want to show the messages
|
* We explicitly require to ignore the faint phase here, as we want to show the messages
|
||||||
* about the critical hit and the super effective/not very effective messages before the faint phase.
|
* about the critical hit and the super effective/not very effective messages before the faint phase.
|
||||||
*/
|
*/
|
||||||
damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, oneHitKo, oneHitKo, true);
|
damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true);
|
||||||
this.turnData.damageTaken += damage.value;
|
this.turnData.damageTaken += damage.value;
|
||||||
if (isCritical) {
|
if (isCritical) {
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
|
this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
|
||||||
@ -2000,7 +2011,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isFainted()) {
|
if (this.isFainted()) {
|
||||||
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), oneHitKo));
|
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
|
||||||
this.resetSummonData();
|
this.resetSummonData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
132
src/test/moves/dragon_rage.test.ts
Normal file
132
src/test/moves/dragon_rage.test.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import GameManager from "#app/test/utils/gameManager";
|
||||||
|
import overrides from "#app/overrides";
|
||||||
|
import { TurnEndPhase } from "#app/phases";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Species } from "#app/enums/species.js";
|
||||||
|
import { Type } from "#app/data/type";
|
||||||
|
import { BattleStat } from "#app/data/battle-stat";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
|
||||||
|
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||||
|
|
||||||
|
describe("Moves - Dragon Rage", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
let partyPokemon: PlayerPokemon;
|
||||||
|
let enemyPokemon: EnemyPokemon;
|
||||||
|
|
||||||
|
const dragonRageDamage = 40;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
|
||||||
|
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
|
||||||
|
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DRAGON_RAGE]);
|
||||||
|
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||||
|
vi.spyOn(overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||||
|
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
|
||||||
|
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||||
|
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||||
|
vi.spyOn(overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||||
|
vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
partyPokemon = game.scene.getParty()[0];
|
||||||
|
enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
|
||||||
|
// remove berries
|
||||||
|
game.scene.removePartyMemberModifiers(0);
|
||||||
|
game.scene.clearEnemyHeldItemModifiers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores weaknesses", async () => {
|
||||||
|
vi.spyOn(enemyPokemon, "getTypes").mockReturnValue([Type.DRAGON]);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
|
||||||
|
|
||||||
|
expect(damageDealt).toBe(dragonRageDamage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores resistances", async () => {
|
||||||
|
vi.spyOn(enemyPokemon, "getTypes").mockReturnValue([Type.STEEL]);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
|
||||||
|
|
||||||
|
expect(damageDealt).toBe(dragonRageDamage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores stat changes", async () => {
|
||||||
|
partyPokemon.summonData.battleStats[BattleStat.SPATK] = 2;
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
|
||||||
|
|
||||||
|
expect(damageDealt).toBe(dragonRageDamage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores stab", async () => {
|
||||||
|
vi.spyOn(partyPokemon, "getTypes").mockReturnValue([Type.DRAGON]);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
|
||||||
|
|
||||||
|
expect(damageDealt).toBe(dragonRageDamage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores criticals", async () => {
|
||||||
|
partyPokemon.removeTag(BattlerTagType.NO_CRIT);
|
||||||
|
partyPokemon.addTag(BattlerTagType.ALWAYS_CRIT, 99);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
|
||||||
|
|
||||||
|
expect(damageDealt).toBe(dragonRageDamage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores damage modification from abilities such as ice scales", async () => {
|
||||||
|
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ICE_SCALES);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
|
||||||
|
|
||||||
|
expect(damageDealt).toBe(dragonRageDamage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores multi hit", async () => {
|
||||||
|
game.scene.addModifier(modifierTypes.MULTI_LENS().newModifier(partyPokemon), false);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp;
|
||||||
|
|
||||||
|
expect(damageDealt).toBe(dragonRageDamage);
|
||||||
|
});
|
||||||
|
});
|
63
src/test/moves/fissure.test.ts
Normal file
63
src/test/moves/fissure.test.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import GameManager from "#app/test/utils/gameManager";
|
||||||
|
import overrides from "#app/overrides";
|
||||||
|
import { DamagePhase } from "#app/phases";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Species } from "#app/enums/species.js";
|
||||||
|
import { EnemyPokemon } from "#app/field/pokemon";
|
||||||
|
|
||||||
|
describe("Moves - Fissure", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
//let partyPokemon: PlayerPokemon;
|
||||||
|
let enemyPokemon: EnemyPokemon;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
|
||||||
|
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
|
||||||
|
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.FISSURE]);
|
||||||
|
vi.spyOn(overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||||
|
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
|
||||||
|
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||||
|
vi.spyOn(overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||||
|
vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
//partyPokemon = game.scene.getParty()[0];
|
||||||
|
enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
|
||||||
|
// remove berries
|
||||||
|
game.scene.removePartyMemberModifiers(0);
|
||||||
|
game.scene.clearEnemyHeldItemModifiers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores damage modification from abilities such as fur coat", async () => {
|
||||||
|
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NO_GUARD);
|
||||||
|
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.FUR_COAT);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.FISSURE));
|
||||||
|
await game.phaseInterceptor.to(DamagePhase, true);
|
||||||
|
|
||||||
|
expect(enemyPokemon.isFainted()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user