[Ability] Implement Power Spot & Battery (#2268)
* add battery * add power spot * refactor * remove FieldVariableMovePowerAbAttr * remove showing ability bar * document + cleanup * add unit tests * update test name * update variable names * update multiplier
This commit is contained in:
parent
eab8db7d29
commit
fd1baef244
|
@ -1282,17 +1282,18 @@ export class VariableMovePowerBoostAbAttr extends VariableMovePowerAbAttr {
|
|||
}
|
||||
}
|
||||
|
||||
export class FieldVariableMovePowerAbAttr extends AbAttr {
|
||||
applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
|
||||
//const power = args[0] as Utils.NumberHolder;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldMovePowerBoostAbAttr extends FieldVariableMovePowerAbAttr {
|
||||
/**
|
||||
* Boosts the power of a Pokémon's move under certain conditions.
|
||||
* @extends AbAttr
|
||||
*/
|
||||
export class FieldMovePowerBoostAbAttr extends AbAttr {
|
||||
private condition: PokemonAttackCondition;
|
||||
private powerMultiplier: number;
|
||||
|
||||
/**
|
||||
* @param condition - A function that determines whether the power boost condition is met.
|
||||
* @param powerMultiplier - The multiplier to apply to the move's power when the condition is met.
|
||||
*/
|
||||
constructor(condition: PokemonAttackCondition, powerMultiplier: number) {
|
||||
super(false);
|
||||
this.condition = condition;
|
||||
|
@ -1310,12 +1311,34 @@ export class FieldMovePowerBoostAbAttr extends FieldVariableMovePowerAbAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boosts the power of a specific type of move.
|
||||
* @extends FieldMovePowerBoostAbAttr
|
||||
*/
|
||||
export class FieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostAbAttr {
|
||||
/**
|
||||
* @param boostedType - The type of move that will receive the power boost.
|
||||
* @param powerMultiplier - The multiplier to apply to the move's power, defaults to 1.5 if not provided.
|
||||
*/
|
||||
constructor(boostedType: Type, powerMultiplier?: number) {
|
||||
super((pokemon, defender, move) => move.type === boostedType, powerMultiplier || 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boosts the power of moves in specified categories.
|
||||
* @extends FieldMovePowerBoostAbAttr
|
||||
*/
|
||||
export class AllyMoveCategoryPowerBoostAbAttr extends FieldMovePowerBoostAbAttr {
|
||||
/**
|
||||
* @param boostedCategories - The categories of moves that will receive the power boost.
|
||||
* @param powerMultiplier - The multiplier to apply to the move's power.
|
||||
*/
|
||||
constructor(boostedCategories: MoveCategory[], powerMultiplier: number) {
|
||||
super((pokemon, defender, move) => boostedCategories.includes(move.category), powerMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
export class BattleStatMultiplierAbAttr extends AbAttr {
|
||||
private battleStat: BattleStat;
|
||||
private multiplier: number;
|
||||
|
@ -4548,7 +4571,7 @@ export function initAbilities() {
|
|||
new Ability(Abilities.DANCER, 7)
|
||||
.attr(PostDancingMoveAbAttr),
|
||||
new Ability(Abilities.BATTERY, 7)
|
||||
.unimplemented(),
|
||||
.attr(AllyMoveCategoryPowerBoostAbAttr, [MoveCategory.SPECIAL], 1.3),
|
||||
new Ability(Abilities.FLUFFY, 7)
|
||||
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 0.5)
|
||||
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.type === Type.FIRE, 2)
|
||||
|
@ -4660,7 +4683,7 @@ export function initAbilities() {
|
|||
.attr(IceFaceMoveImmunityAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE))
|
||||
.ignorable(),
|
||||
new Ability(Abilities.POWER_SPOT, 8)
|
||||
.unimplemented(),
|
||||
.attr(AllyMoveCategoryPowerBoostAbAttr, [MoveCategory.SPECIAL, MoveCategory.PHYSICAL], 1.3),
|
||||
new Ability(Abilities.MIMICRY, 8)
|
||||
.unimplemented(),
|
||||
new Ability(Abilities.SCREEN_CLEANER, 8)
|
||||
|
|
|
@ -22,7 +22,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStat
|
|||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, FieldMoveTypePowerBoostAbAttr } from "../data/ability";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr } from "../data/ability";
|
||||
import PokemonData from "../system/pokemon-data";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { Mode } from "../ui/ui";
|
||||
|
@ -1755,6 +1755,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
}
|
||||
applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, this, move, power);
|
||||
|
||||
if (source.getAlly()?.hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) {
|
||||
applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source, this, move, power);
|
||||
}
|
||||
|
||||
const fieldAuras = new Set(
|
||||
this.scene.getField(true)
|
||||
.map((p) => p.getAbilityAttrs(FieldMoveTypePowerBoostAbAttr) as FieldMoveTypePowerBoostAbAttr[])
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import * as overrides from "#app/overrides";
|
||||
import { Species } from "#enums/species";
|
||||
import { TurnEndPhase, } from "#app/phases";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import Move, { allMoves, MoveCategory } from "#app/data/move.js";
|
||||
import { AllyMoveCategoryPowerBoostAbAttr } from "#app/data/ability.js";
|
||||
import { NumberHolder } from "#app/utils.js";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
|
||||
describe("Abilities - Battery", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.ROCK_SLIDE, Moves.SPLASH, Moves.HEAT_WAVE]);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
});
|
||||
|
||||
it("raises the power of allies' special moves by 30%", async () => {
|
||||
const moveToBeUsed = Moves.HEAT_WAVE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
|
||||
await game.startBattle([Species.MAGIKARP, Species.CHARJABUG]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]);
|
||||
const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(appliedPower).not.toBe(undefined);
|
||||
expect(appliedPower).not.toBe(basePower);
|
||||
expect(appliedPower).toBe(basePower * multiplier);
|
||||
});
|
||||
|
||||
it("does not raise the power of allies' non-special moves", async () => {
|
||||
const moveToBeUsed = Moves.ROCK_SLIDE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
|
||||
await game.startBattle([Species.MAGIKARP, Species.CHARJABUG]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]);
|
||||
const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(appliedPower).not.toBe(undefined);
|
||||
expect(appliedPower).toBe(basePower);
|
||||
expect(appliedPower).not.toBe(basePower * multiplier);
|
||||
});
|
||||
|
||||
it("does not raise the power of the ability owner's special moves", async () => {
|
||||
const moveToBeUsed = Moves.HEAT_WAVE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
|
||||
await game.startBattle([Species.CHARJABUG, Species.MAGIKARP]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[0]);
|
||||
const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(appliedPower).not.toBe(undefined);
|
||||
expect(appliedPower).toBe(basePower);
|
||||
expect(appliedPower).not.toBe(basePower * multiplier);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Calculates the adjusted applied power of a move.
|
||||
*
|
||||
* @param defender - The defending Pokémon.
|
||||
* @param attacker - The attacking Pokémon.
|
||||
* @param move - The move being used by the attacker.
|
||||
* @returns The adjusted power of the move.
|
||||
*/
|
||||
const getAppliedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => {
|
||||
const powerHolder = new NumberHolder(move.power);
|
||||
|
||||
/**
|
||||
* @see AllyMoveCategoryPowerBoostAbAttr
|
||||
*/
|
||||
if (attacker.getAlly().hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) {
|
||||
const batteryInstance = new AllyMoveCategoryPowerBoostAbAttr([MoveCategory.SPECIAL], 1.3);
|
||||
batteryInstance.applyPreAttack(attacker, false, defender, move, [ powerHolder ]);
|
||||
}
|
||||
|
||||
return powerHolder.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the power multiplier from a Pokémon's ability attribute.
|
||||
*
|
||||
* @param pokemon - The Pokémon whose ability attributes are being queried.
|
||||
* @returns The power multiplier of the `AllyMoveCategoryPowerBoostAbAttr` attribute.
|
||||
*/
|
||||
const getAttrPowerMultiplier = (pokemon: Pokemon) => {
|
||||
const attr = pokemon.getAbilityAttrs(AllyMoveCategoryPowerBoostAbAttr);
|
||||
|
||||
return (attr[0] as AllyMoveCategoryPowerBoostAbAttr)["powerMultiplier"];
|
||||
};
|
|
@ -0,0 +1,125 @@
|
|||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import * as overrides from "#app/overrides";
|
||||
import { Species } from "#enums/species";
|
||||
import { TurnEndPhase, } from "#app/phases";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import Move, { allMoves, MoveCategory } from "#app/data/move.js";
|
||||
import { AllyMoveCategoryPowerBoostAbAttr } from "#app/data/ability.js";
|
||||
import { NumberHolder } from "#app/utils.js";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
|
||||
describe("Abilities - Power Spot", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.ROCK_SLIDE, Moves.SPLASH, Moves.HEAT_WAVE]);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
});
|
||||
|
||||
it("raises the power of allies' special moves by 30%", async () => {
|
||||
const moveToBeUsed = Moves.HEAT_WAVE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
|
||||
await game.startBattle([Species.MAGIKARP, Species.STONJOURNER]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]);
|
||||
const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(appliedPower).not.toBe(undefined);
|
||||
expect(appliedPower).not.toBe(basePower);
|
||||
expect(appliedPower).toBe(basePower * multiplier);
|
||||
});
|
||||
|
||||
it("raises the power of allies' physical moves by 30%", async () => {
|
||||
const moveToBeUsed = Moves.ROCK_SLIDE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
|
||||
await game.startBattle([Species.MAGIKARP, Species.STONJOURNER]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]);
|
||||
const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(appliedPower).not.toBe(undefined);
|
||||
expect(appliedPower).not.toBe(basePower);
|
||||
expect(appliedPower).toBe(basePower * multiplier);
|
||||
});
|
||||
|
||||
it("does not raise the power of the ability owner's moves", async () => {
|
||||
const moveToBeUsed = Moves.HEAT_WAVE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
|
||||
await game.startBattle([Species.STONJOURNER, Species.MAGIKARP]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[0]);
|
||||
const appliedPower = getAppliedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(appliedPower).not.toBe(undefined);
|
||||
expect(appliedPower).toBe(basePower);
|
||||
expect(appliedPower).not.toBe(basePower * multiplier);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Calculates the adjusted applied power of a move.
|
||||
*
|
||||
* @param defender - The defending Pokémon.
|
||||
* @param attacker - The attacking Pokémon.
|
||||
* @param move - The move being used by the attacker.
|
||||
* @returns The adjusted power of the move.
|
||||
*/
|
||||
const getAppliedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => {
|
||||
const powerHolder = new NumberHolder(move.power);
|
||||
|
||||
/**
|
||||
* @see AllyMoveCategoryPowerBoostAbAttr
|
||||
*/
|
||||
if (attacker.getAlly().hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) {
|
||||
const powerSpotInstance = new AllyMoveCategoryPowerBoostAbAttr([MoveCategory.SPECIAL, MoveCategory.PHYSICAL], 1.3);
|
||||
powerSpotInstance.applyPreAttack(attacker, false, defender, move, [ powerHolder ]);
|
||||
}
|
||||
|
||||
return powerHolder.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the power multiplier from a Pokémon's ability attribute.
|
||||
*
|
||||
* @param pokemon - The Pokémon whose ability attributes are being queried.
|
||||
* @returns The power multiplier of the `AllyMoveCategoryPowerBoostAbAttr` attribute.
|
||||
*/
|
||||
const getAttrPowerMultiplier = (pokemon: Pokemon) => {
|
||||
const attr = pokemon.getAbilityAttrs(AllyMoveCategoryPowerBoostAbAttr);
|
||||
|
||||
return (attr[0] as AllyMoveCategoryPowerBoostAbAttr)["powerMultiplier"];
|
||||
};
|
Loading…
Reference in New Issue