diff --git a/src/data/ability.ts b/src/data/ability.ts index eb55f661e66..4f4807ed614 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1446,7 +1446,7 @@ export class FieldMovePowerBoostAbAttr extends AbAttr { * Boosts the power of a specific type of move. * @extends FieldMovePowerBoostAbAttr */ -export class FieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostAbAttr { +export class PreAttackFieldMoveTypePowerBoostAbAttr 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. @@ -1456,6 +1456,18 @@ export class FieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostAbAttr { } } +/** + * Boosts the power of a specific type of move for all Pokemon in the field. + * @extends PreAttackFieldMoveTypePowerBoostAbAttr + */ +export class FieldMoveTypePowerBoostAbAttr extends PreAttackFieldMoveTypePowerBoostAbAttr { } + +/** + * Boosts the power of a specific type of move for the user and its allies. + * @extends PreAttackFieldMoveTypePowerBoostAbAttr + */ +export class UserFieldMoveTypePowerBoostAbAttr extends PreAttackFieldMoveTypePowerBoostAbAttr { } + /** * Boosts the power of moves in specified categories. * @extends FieldMovePowerBoostAbAttr @@ -4964,8 +4976,7 @@ export function initAbilities() { new Ability(Abilities.SCREEN_CLEANER, 8) .attr(PostSummonRemoveArenaTagAbAttr, [ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT]), new Ability(Abilities.STEELY_SPIRIT, 8) - .attr(MoveTypePowerBoostAbAttr, Type.STEEL) - .partial(), + .attr(UserFieldMoveTypePowerBoostAbAttr, Type.STEEL), new Ability(Abilities.PERISH_BODY, 8) .attr(PostDefendPerishSongAbAttr, 4), new Ability(Abilities.WANDERING_SPIRIT, 8) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 88d7f6b73a8..2c05492c2c6 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag 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, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr } 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, AddSecondStrikeAbAttr, UserFieldMoveTypePowerBoostAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1788,6 +1788,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { aura.applyPreAttack(null, null, null, move, [power]); } + const alliedField: Pokemon[] = source instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField(); + alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, this, move, power)); + power.value *= typeChangeMovePowerMultiplier.value; if (!typeless) { diff --git a/src/test/abilities/steely_spirit.test.ts b/src/test/abilities/steely_spirit.test.ts new file mode 100644 index 00000000000..e9c673a102a --- /dev/null +++ b/src/test/abilities/steely_spirit.test.ts @@ -0,0 +1,115 @@ +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 { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon.js"; +import Move, { allMoves } from "#app/data/move.js"; +import { NumberHolder } from "#app/utils.js"; +import { allAbilities, applyPreAttackAbAttrs, UserFieldMoveTypePowerBoostAbAttr } from "#app/data/ability.js"; +import { Abilities } from "#app/enums/abilities.js"; + +describe("Abilities - Steely Spirit", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const steelySpiritMultiplier = 1.5; + const moveToCheck = Moves.IRON_HEAD; + + 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, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.IRON_HEAD, Moves.SPLASH]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + }); + + it("increases Steel-type moves used by the user and its allies", async () => { + await game.startBattle([Species.MAGIKARP, Species.PERRSERKER]); + const perserrker = game.scene.getPlayerField()[1]; + + vi.spyOn(perserrker, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); + + expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), perserrker, allMoves[moveToCheck]); + + expect(mockedMovePower).toBe(allMoves[moveToCheck].power * steelySpiritMultiplier); + }); + + it("stacks if multiple users with this ability are on the field.", async () => { + await game.startBattle([Species.PERRSERKER, Species.PERRSERKER]); + + game.scene.getPlayerField().forEach(p => { + vi.spyOn(p, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); + }); + + expect(game.scene.getPlayerField().every(p => p.hasAbility(Abilities.STEELY_SPIRIT))).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToCheck]); + + expect(mockedMovePower).toBe(allMoves[moveToCheck].power * Math.pow(steelySpiritMultiplier, 2)); + }); + + it("does not take effect when suppressed", async () => { + await game.startBattle([Species.MAGIKARP, Species.PERRSERKER]); + const perserrker = game.scene.getPlayerField()[1]; + + vi.spyOn(perserrker, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); + expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true); + + perserrker.summonData.abilitySuppressed = true; + + expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(false); + expect(perserrker.summonData.abilitySuppressed).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), perserrker, allMoves[moveToCheck]); + + expect(mockedMovePower).toBe(allMoves[moveToCheck].power); + }); +}); + +/** + * Calculates the mocked power of a move. + * Note this does not consider other damage calculations + * except the power multiplier from Steely Spirit. + * + * @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 getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => { + const powerHolder = new NumberHolder(move.power); + + /** + * Check if pokemon has the specified ability and is in effect. + * See Pokemon.hasAbility {@linkcode Pokemon.hasAbility} + */ + if (attacker.hasAbility(Abilities.STEELY_SPIRIT)) { + const alliedField: Pokemon[] = attacker instanceof PlayerPokemon ? attacker.scene.getPlayerField() : attacker.scene.getEnemyField(); + alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, this, move, powerHolder)); + } + + return powerHolder.value; +};