[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:
Adrian T 2024-06-18 04:12:11 +08:00 committed by GitHub
parent eab8db7d29
commit fd1baef244
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 288 additions and 11 deletions

View File

@ -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)

View File

@ -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[])

View File

@ -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"];
};

View File

@ -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"];
};