mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2024-11-25 16:26:25 +00:00
[Move] Improved damage forecasting for Shell Side Arm (#4310)
This commit is contained in:
parent
81ea1296b3
commit
605ae9e1c3
@ -3974,18 +3974,17 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr {
|
||||
export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const category = (args[0] as Utils.NumberHolder);
|
||||
const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move);
|
||||
const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move);
|
||||
|
||||
// Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games
|
||||
if (atkRatio > specialRatio) {
|
||||
const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true);
|
||||
const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true);
|
||||
|
||||
if (predictedPhysDmg > predictedSpecDmg) {
|
||||
category.value = MoveCategory.PHYSICAL;
|
||||
return true;
|
||||
} else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) {
|
||||
} else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) {
|
||||
category.value = MoveCategory.PHYSICAL;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -9106,7 +9105,7 @@ export function initMoves() {
|
||||
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
|
||||
.attr(ShellSideArmCategoryAttr)
|
||||
.attr(StatusEffectAttr, StatusEffect.POISON)
|
||||
.partial(),
|
||||
.partial(), // Physical version of the move does not make contact
|
||||
new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8)
|
||||
.attr(SacrificialAttr)
|
||||
.target(MoveTarget.ALL_NEAR_OTHERS)
|
||||
|
@ -2322,11 +2322,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return accuracyMultiplier.value / evasionMultiplier.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param source the attacking {@linkcode Pokemon}.
|
||||
* @param move the {@linkcode Move} used in the attack.
|
||||
* @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 ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (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`).
|
||||
* @returns The move's base damage against this Pokemon when used by the source Pokemon.
|
||||
*/
|
||||
getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number {
|
||||
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
||||
|
||||
/** A base damage multiplier based on the source's level */
|
||||
const levelMultiplier = (2 * source.level / 5 + 2);
|
||||
|
||||
/** The power of the move after power boosts from abilities, etc. have applied */
|
||||
const power = move.calculateBattlePower(source, this, simulated);
|
||||
|
||||
/**
|
||||
* The attacker's offensive stat for the given move's category.
|
||||
* Critical hits cause negative stat stages to be ignored.
|
||||
*/
|
||||
const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated));
|
||||
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
|
||||
|
||||
/**
|
||||
* This Pokemon's defensive stat for the given move's category.
|
||||
* Critical hits cause positive stat stages to be ignored.
|
||||
*/
|
||||
const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated));
|
||||
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2;
|
||||
|
||||
/** Debug message for non-simulated calls (i.e. when damage is actually dealt) */
|
||||
if (!simulated) {
|
||||
console.log("base damage", baseDamage, move.name, power, sourceAtk.value, targetDef.value);
|
||||
}
|
||||
|
||||
return baseDamage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the damage of an attack made by another Pokemon against this Pokemon
|
||||
* @param source {@linkcode Pokemon} the attacking Pokemon
|
||||
* @param move {@linkcode Pokemon} the move used in the attack
|
||||
* @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects
|
||||
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
|
||||
* @param isCritical If `true`, calculates damage for a critical hit.
|
||||
* @param simulated If `true`, suppresses changes to game state during the calculation.
|
||||
* @returns a {@linkcode DamageCalculationResult} object with three fields:
|
||||
@ -2395,35 +2445,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
};
|
||||
}
|
||||
|
||||
// ----- BEGIN BASE DAMAGE MULTIPLIERS -----
|
||||
|
||||
/** A base damage multiplier based on the source's level */
|
||||
const levelMultiplier = (2 * source.level / 5 + 2);
|
||||
|
||||
/** The power of the move after power boosts from abilities, etc. have applied */
|
||||
const power = move.calculateBattlePower(source, this, simulated);
|
||||
|
||||
/**
|
||||
* The attacker's offensive stat for the given move's category.
|
||||
* Critical hits ignore negative stat stages.
|
||||
*/
|
||||
const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated));
|
||||
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
|
||||
|
||||
/**
|
||||
* This Pokemon's defensive stat for the given move's category.
|
||||
* Critical hits ignore positive stat stages.
|
||||
*/
|
||||
const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated));
|
||||
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2;
|
||||
|
||||
// ------ END BASE DAMAGE MULTIPLIERS ------
|
||||
const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated);
|
||||
|
||||
/** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */
|
||||
const { targets, multiple } = getMoveTargets(source, move.id);
|
||||
@ -2549,7 +2575,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
// debug message for when damage is applied (i.e. not simulated)
|
||||
if (!simulated) {
|
||||
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef);
|
||||
console.log("damage", damage.value, move.name);
|
||||
}
|
||||
|
||||
let hitResult: HitResult;
|
||||
|
87
src/test/moves/shell_side_arm.test.ts
Normal file
87
src/test/moves/shell_side_arm.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves, ShellSideArmCategoryAttr } from "#app/data/move";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
|
||||
|
||||
describe("Moves - Shell Side Arm", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([Moves.SHELL_SIDE_ARM])
|
||||
.battleType("single")
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("becomes a physical attack if forecasted to deal more damage as physical", async () => {
|
||||
game.override.enemySpecies(Species.SNORLAX);
|
||||
|
||||
await game.classicMode.startBattle([Species.MANAPHY]);
|
||||
|
||||
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
|
||||
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
|
||||
vi.spyOn(shellSideArmAttr, "apply");
|
||||
|
||||
game.move.select(Moves.SHELL_SIDE_ARM);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(true);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("remains a special attack if forecasted to deal more damage as special", async () => {
|
||||
game.override.enemySpecies(Species.SLOWBRO);
|
||||
|
||||
await game.classicMode.startBattle([Species.MANAPHY]);
|
||||
|
||||
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
|
||||
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
|
||||
vi.spyOn(shellSideArmAttr, "apply");
|
||||
|
||||
game.move.select(Moves.SHELL_SIDE_ARM);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("respects stat stage changes when forecasting base damage", async () => {
|
||||
game.override
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyMoveset(Moves.COTTON_GUARD);
|
||||
|
||||
await game.classicMode.startBattle([Species.MANAPHY]);
|
||||
|
||||
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
|
||||
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
|
||||
vi.spyOn(shellSideArmAttr, "apply");
|
||||
|
||||
game.move.select(Moves.SHELL_SIDE_ARM);
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false);
|
||||
}, TIMEOUT);
|
||||
});
|
Loading…
Reference in New Issue
Block a user