Refactor parameter list for pokemon#getBaseDamage and pokemon#getAttackDamage

This commit is contained in:
Sirz Benjie 2025-04-11 10:09:20 -05:00
parent a1cb77a8da
commit 607dc70b9b
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
8 changed files with 117 additions and 65 deletions

View File

@ -7410,4 +7410,4 @@ export function initAbilities() {
.unreplaceable() // TODO is this true? .unreplaceable() // TODO is this true?
.attr(ConfusionOnStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .attr(ConfusionOnStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
); );
} }

View File

@ -4811,8 +4811,8 @@ export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as NumberHolder); const category = (args[0] as NumberHolder);
const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true, true, true); const predictedPhysDmg = target.getBaseDamage({source: user, move, moveCategory: MoveCategory.PHYSICAL, ignoreAbility: true, ignoreSourceAbility: true, ignoreAllyAbility: true, ignoreSourceAllyAbility: true, simulated: true});
const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true, true, true); const predictedSpecDmg = target.getBaseDamage({source: user, move, moveCategory: MoveCategory.SPECIAL, ignoreAbility: true, ignoreSourceAbility: true, ignoreAllyAbility: true, ignoreSourceAllyAbility: true, simulated: true});
if (predictedPhysDmg > predictedSpecDmg) { if (predictedPhysDmg > predictedSpecDmg) {
category.value = MoveCategory.PHYSICAL; category.value = MoveCategory.PHYSICAL;

View File

@ -277,6 +277,36 @@ export enum FieldPosition {
RIGHT, RIGHT,
} }
/** Base typeclass for damage parameter methods, used for DRY */
type damageParams = {
/** The attacking {@linkcode Pokemon} */
source: Pokemon;
/** The move used in the attack */
move: Move;
/** The move's {@linkcode MoveCategory} after variable-category effects are applied */
moveCategory: MoveCategory;
/** If `true`, ignores this Pokemon's defensive ability effects */
ignoreAbility?: boolean;
/** If `true`, ignores the attacking Pokemon's ability effects */
ignoreSourceAbility?: boolean;
/** If `true`, ignores the ally Pokemon's ability effects */
ignoreAllyAbility?: boolean;
/** If `true`, ignores the ability effects of the attacking pokemon's ally */
ignoreSourceAllyAbility?: boolean;
/** If `true`, calculates damage for a critical hit */
isCritical?: boolean;
/** If `true`, suppresses changes to game state during the calculation */
simulated?: boolean;
/** If defined, used in place of calculated effectiveness values */
effectiveness?: number;
}
/** Type for the parameters of {@linkcode Pokemon#getBaseDamage getBaseDamage} */
type getBaseDamageParams = Omit<damageParams, "effectiveness">
/** Type for the parameters of {@linkcode Pokemon#getAttackDamage getAttackDamage} */
type getAttackDamageParams = Omit<damageParams, "moveCategory">;
export default abstract class Pokemon extends Phaser.GameObjects.Container { export default abstract class Pokemon extends Phaser.GameObjects.Container {
public id: number; public id: number;
public name: string; public name: string;
@ -1441,25 +1471,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Calculate the critical-hit stage of a move used against this pokemon by * Calculate the critical-hit stage of a move used against this pokemon by
* the given source * the given source
* *
* @param source the {@linkcode Pokemon} who using the move * @param source - The {@linkcode Pokemon} who using the move
* @param move the {@linkcode Move} being used * @param move - The {@linkcode Move} being used
* @returns the final critical-hit stage value * @returns The final critical-hit stage value
*/ */
getCritStage(source: Pokemon, move: Move): number { getCritStage(source: Pokemon, move: Move): number {
const critStage = new NumberHolder(0); const critStage = new NumberHolder(0);
applyMoveAttrs(HighCritAttr, source, this, move, critStage); applyMoveAttrs(HighCritAttr, source, this, move, critStage);
globalScene.applyModifiers( globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
CritBoosterModifier, globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
source.isPlayer(), applyAbAttrs(BonusCritAbAttr, source, null, false, critStage);
source,
critStage,
);
globalScene.applyModifiers(
TempCritBoosterModifier,
source.isPlayer(),
critStage,
);
applyAbAttrs(BonusCritAbAttr, source, null, false, critStage)
const critBoostTag = source.getTag(CritBoostTag); const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) { if (critBoostTag) {
if (critBoostTag instanceof DragonCheerTag) { if (critBoostTag instanceof DragonCheerTag) {
@ -1475,6 +1496,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return critStage.value; return critStage.value;
} }
/**
* Calculates the category of a move when used by this pokemon after
* category-changing move effects are applied.
* @param target - The {@linkcode Pokemon} using the move
* @param move - The {@linkcode Move} being used
* @returns The given move's final category
*/
getMoveCategory(target: Pokemon, move: Move): MoveCategory {
const moveCategory = new Utils.NumberHolder(move.category);
applyMoveAttrs(VariableMoveCategoryAttr, this, target, move, moveCategory);
return moveCategory.value;
}
/** /**
* Calculates and retrieves the final value of a stat considering any held * Calculates and retrieves the final value of a stat considering any held
* items, move effects, opponent abilities, and whether there was a critical * items, move effects, opponent abilities, and whether there was a critical
@ -4075,27 +4109,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Calculates the base damage of the given move against this Pokemon when attacked by the given source. * Calculates the base damage of the given move against this Pokemon when attacked by the given source.
* Used during damage calculation and for Shell Side Arm's forecasting effect. * Used during damage calculation and for Shell Side Arm's forecasting effect.
* @param source the attacking {@linkcode Pokemon}. * @param source - The attacking {@linkcode Pokemon}.
* @param move the {@linkcode Move} used in the attack. * @param move - The {@linkcode Move} used in the attack.
* @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied. * @param moveCategory - The move's {@linkcode MoveCategory} after variable-category effects are applied.
* @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`). * @param ignoreAbility - If `true`, ignores this Pokemon's defensive ability effects (defaults to `false`).
* @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). * @param ignoreSourceAbility - If `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`).
* @param ignoreAllyAbility if `true`, ignores the ally Pokemon's ability effects (defaults to `false`). * @param ignoreAllyAbility - If `true`, ignores the ally Pokemon's ability effects (defaults to `false`).
* @param ignoreSourceAllyAbility if `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`). * @param ignoreSourceAllyAbility - If `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`).
* @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`). * @param isCritical - if `true`, calculates effective stats as if the hit were critical (defaults to `false`).
* @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`). * @param simulated - if `true`, suppresses changes to game state during calculation (defaults to `true`).
* @returns The move's base damage against this Pokemon when used by the source Pokemon. * @returns The move's base damage against this Pokemon when used by the source Pokemon.
*/ */
getBaseDamage( getBaseDamage(
source: Pokemon, {
move: Move, source,
moveCategory: MoveCategory, move,
moveCategory,
ignoreAbility = false, ignoreAbility = false,
ignoreSourceAbility = false, ignoreSourceAbility = false,
ignoreAllyAbility = false, ignoreAllyAbility = false,
ignoreSourceAllyAbility = false, ignoreSourceAllyAbility = false,
isCritical = false, isCritical = false,
simulated = true, simulated = true}: getBaseDamageParams
): number { ): number {
const isPhysical = moveCategory === MoveCategory.PHYSICAL; const isPhysical = moveCategory === MoveCategory.PHYSICAL;
@ -4222,27 +4257,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Calculates the damage of an attack made by another Pokemon against this Pokemon * Calculates the damage of an attack made by another Pokemon against this Pokemon
* @param source {@linkcode Pokemon} the attacking Pokemon * @param source {@linkcode Pokemon} the attacking Pokemon
* @param move {@linkcode Pokemon} the move used in the attack * @param move The {@linkcode Move} used in the attack
* @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
* @param ignoreAllyAbility If `true`, ignores the ally Pokemon's ability effects * @param ignoreAllyAbility If `true`, ignores the ally Pokemon's ability effects
* @param ignoreSourceAllyAbility If `true`, ignores the ability effects of the attacking pokemon's ally * @param ignoreSourceAllyAbility If `true`, ignores the ability effects of the attacking pokemon's ally
* @param isCritical If `true`, calculates damage for a critical hit. * @param isCritical If `true`, calculates damage for a critical hit.
* @param simulated If `true`, suppresses changes to game state during the calculation. * @param simulated If `true`, suppresses changes to game state during the calculation.
* @returns a {@linkcode DamageCalculationResult} object with three fields: * @param effectiveness If defined, used in place of calculated effectiveness values
* - `cancelled`: `true` if the move was cancelled by another effect. * @returns The {@linkcode DamageCalculationResult}
* - `result`: {@linkcode HitResult} indicates the attack's type effectiveness.
* - `damage`: `number` the attack's final damage output.
*/ */
getAttackDamage( getAttackDamage(
source: Pokemon, {
move: Move, source,
ignoreAbility = false, move,
ignoreSourceAbility = false, ignoreAbility = false,
ignoreAllyAbility = false, ignoreSourceAbility = false,
ignoreSourceAllyAbility = false, ignoreAllyAbility = false,
isCritical = false, ignoreSourceAllyAbility = false,
simulated = true, isCritical = false,
simulated = true,
effectiveness}: getAttackDamageParams,
): DamageCalculationResult { ): DamageCalculationResult {
const damage = new NumberHolder(0); const damage = new NumberHolder(0);
const defendingSide = this.isPlayer() const defendingSide = this.isPlayer()
@ -4272,7 +4307,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* *
* Note that the source's abilities are not ignored here * Note that the source's abilities are not ignored here
*/ */
const typeMultiplier = this.getMoveEffectiveness( const typeMultiplier = effectiveness ?? this.getMoveEffectiveness(
source, source,
move, move,
ignoreAbility, ignoreAbility,
@ -4344,7 +4379,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* The attack's base damage, as determined by the source's level, move power * The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat * and Attack stat as well as this Pokemon's Defense stat
*/ */
const baseDamage = this.getBaseDamage( const baseDamage = this.getBaseDamage({
source, source,
move, move,
moveCategory, moveCategory,
@ -4354,7 +4389,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
ignoreSourceAllyAbility, ignoreSourceAllyAbility,
isCritical, isCritical,
simulated, simulated,
); });
/** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */
const { targets, multiple } = getMoveTargets(source, move.id); const { targets, multiple } = getMoveTargets(source, move.id);
@ -4637,7 +4672,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
cancelled, cancelled,
result, result,
damage: dmg, damage: dmg,
} = this.getAttackDamage(source, move, false, false, false, false, isCritical, false); } = this.getAttackDamage(
{source, move, isCritical, simulated: false});
const typeBoost = source.findTag( const typeBoost = source.findTag(
t => t =>
@ -7311,14 +7347,15 @@ export class EnemyPokemon extends Pokemon {
].includes(move.id); ].includes(move.id);
return ( return (
doesNotFail && doesNotFail &&
p.getAttackDamage( p.getAttackDamage({
this, source: this,
move, move,
!p.battleData.abilityRevealed, ignoreAbility: !p.battleData.abilityRevealed,
false, ignoreSourceAbility: false,
!p.getAlly()?.battleData.abilityRevealed, ignoreAllyAbility: !p.getAlly()?.battleData.abilityRevealed,
false, ignoreSourceAllyAbility: false,
isCritical, isCritical,
}
).damage >= p.hp ).damage >= p.hp
); );
}) })

View File

@ -50,7 +50,11 @@ describe("Moves - Friend Guard", () => {
// Get the last return value from `getAttackDamage` // Get the last return value from `getAttackDamage`
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
// Making sure the test is controlled; turn 1 damage is equal to base damage (after rounding) // Making sure the test is controlled; turn 1 damage is equal to base damage (after rounding)
expect(turn1Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL))); expect(turn1Damage).toBe(
Math.floor(
player1.getBaseDamage({ source: enemy1, move: allMoves[Moves.TACKLE], moveCategory: MoveCategory.PHYSICAL }),
),
);
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]); vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
@ -64,7 +68,10 @@ describe("Moves - Friend Guard", () => {
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
// With the ally's Friend Guard, damage should have been reduced from base damage by 25% // With the ally's Friend Guard, damage should have been reduced from base damage by 25%
expect(turn2Damage).toBe( expect(turn2Damage).toBe(
Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL) * 0.75), Math.floor(
player1.getBaseDamage({ source: enemy1, move: allMoves[Moves.TACKLE], moveCategory: MoveCategory.PHYSICAL }) *
0.75,
),
); );
}); });

View File

@ -61,11 +61,11 @@ describe("Abilities - Infiltrator", () => {
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
const preScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage; const preScreenDmg = enemy.getAttackDamage({ source: player, move: allMoves[move] }).damage;
game.scene.arena.addTag(tagType, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true); game.scene.arena.addTag(tagType, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
const postScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage; const postScreenDmg = enemy.getAttackDamage({ source: player, move: allMoves[move] }).damage;
expect(postScreenDmg).toBe(preScreenDmg); expect(postScreenDmg).toBe(preScreenDmg);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR); expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);

View File

@ -47,7 +47,9 @@ describe("Battle Mechanics - Damage Calculation", () => {
// expected base damage = [(2*level/5 + 2) * power * playerATK / enemyDEF / 50] + 2 // expected base damage = [(2*level/5 + 2) * power * playerATK / enemyDEF / 50] + 2
// = 31.8666... // = 31.8666...
expect(enemyPokemon.getAttackDamage(playerPokemon, allMoves[Moves.TACKLE]).damage).toBeCloseTo(31); expect(enemyPokemon.getAttackDamage({ source: playerPokemon, move: allMoves[Moves.TACKLE] }).damage).toBeCloseTo(
31,
);
}); });
it("Attacks deal 1 damage at minimum", async () => { it("Attacks deal 1 damage at minimum", async () => {
@ -91,7 +93,7 @@ describe("Battle Mechanics - Damage Calculation", () => {
const magikarp = game.scene.getPlayerPokemon()!; const magikarp = game.scene.getPlayerPokemon()!;
const dragonite = game.scene.getEnemyPokemon()!; const dragonite = game.scene.getEnemyPokemon()!;
expect(dragonite.getAttackDamage(magikarp, allMoves[Moves.DRAGON_RAGE]).damage).toBe(40); expect(dragonite.getAttackDamage({ source: magikarp, move: allMoves[Moves.DRAGON_RAGE] }).damage).toBe(40);
}); });
it("One-hit KO moves ignore damage multipliers", async () => { it("One-hit KO moves ignore damage multipliers", async () => {
@ -102,7 +104,7 @@ describe("Battle Mechanics - Damage Calculation", () => {
const magikarp = game.scene.getPlayerPokemon()!; const magikarp = game.scene.getPlayerPokemon()!;
const aggron = game.scene.getEnemyPokemon()!; const aggron = game.scene.getEnemyPokemon()!;
expect(aggron.getAttackDamage(magikarp, allMoves[Moves.FISSURE]).damage).toBe(aggron.hp); expect(aggron.getAttackDamage({ source: magikarp, move: allMoves[Moves.FISSURE] }).damage).toBe(aggron.hp);
}); });
it("When the user fails to use Jump Kick with Wonder Guard ability, the damage should be 1.", async () => { it("When the user fails to use Jump Kick with Wonder Guard ability, the damage should be 1.", async () => {

View File

@ -97,14 +97,20 @@ describe("Moves - Dig", () => {
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
const preDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage; const preDigEarthquakeDmg = playerPokemon.getAttackDamage({
source: enemyPokemon,
move: allMoves[Moves.EARTHQUAKE],
}).damage;
game.move.select(Moves.DIG); game.move.select(Moves.DIG);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase"); await game.phaseInterceptor.to("MoveEffectPhase");
const postDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage; const postDigEarthquakeDmg = playerPokemon.getAttackDamage({
source: enemyPokemon,
move: allMoves[Moves.EARTHQUAKE],
}).damage;
// these hopefully get avoid rounding errors :shrug: // these hopefully get avoid rounding errors :shrug:
expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg); expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg);
expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1)); expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1));

View File

@ -71,7 +71,7 @@ describe("Moves - Spectral Thief", () => {
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
const moveToCheck = allMoves[Moves.SPECTRAL_THIEF]; const moveToCheck = allMoves[Moves.SPECTRAL_THIEF];
const dmgBefore = enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage; const dmgBefore = enemy.getAttackDamage({ source: player, move: moveToCheck }).damage;
enemy.setStatStage(Stat.ATK, 6); enemy.setStatStage(Stat.ATK, 6);
@ -80,7 +80,7 @@ describe("Moves - Spectral Thief", () => {
game.move.select(Moves.SPECTRAL_THIEF); game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(dmgBefore).toBeLessThan(enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage); expect(dmgBefore).toBeLessThan(enemy.getAttackDamage({ source: player, move: moveToCheck }).damage);
}); });
it("should steal stat stages as a negative value with Contrary.", async () => { it("should steal stat stages as a negative value with Contrary.", async () => {