From fac20ca97a214acbdd83d0f1795bf73d499f5186 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:24:19 -0500 Subject: [PATCH] [Ability] Fully implement Flower Gift and Victory Star (#5222) * Fully implement Flower Gift and Victory Star * Fully implement Flower Gift and Victory Star * Update src/field/pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/field/pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Accept suggested change Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Accept suggested change Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/field/pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fix check for ignore_abilities move flag * Fix missing argument to getBaseDamage in getAttackDamage * Fix merge conflict due to same changed import line * Fix call to getAttackDamage that was reset after merge * Update calls to getEffectiveStat --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 88 ++++++++++++++++++++++++- src/data/battler-tags.ts | 10 ++- src/data/moves/move.ts | 4 +- src/field/pokemon.ts | 34 +++++++++- test/abilities/flower_gift.test.ts | 95 ++++++++++++++++++++++++--- test/abilities/protosynthesis.test.ts | 48 ++++++++++++-- test/abilities/victory_star.test.ts | 60 +++++++++++++++++ 7 files changed, 317 insertions(+), 22 deletions(-) create mode 100644 test/abilities/victory_star.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 5bf02cabf6c..6ffdc1f5403 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1712,6 +1712,62 @@ export class PostAttackAbAttr extends AbAttr { args: any[]): void {} } +/** + * Multiplies a Stat from an ally pokemon's ability. + * @see {@link applyAllyStatMultiplierAbAttrs} + * @see {@link applyAllyStat} + */ +export class AllyStatMultiplierAbAttr extends AbAttr { + private stat: BattleStat; + private multiplier: number; + private ignorable: boolean; + + /** + * @param stat - The stat being modified + * @param multipler - The multiplier to apply to the stat + * @param ignorable - Whether the multiplier can be ignored by mold breaker-like moves and abilities + */ + constructor(stat: BattleStat, multiplier: number, ignorable: boolean = true) { + super(false); + + this.stat = stat; + this.multiplier = multiplier; + this.ignorable = ignorable; + } + + /** + * Multiply a Pokemon's Stat due to an Ally's ability. + * @param _pokemon - The ally {@linkcode Pokemon} with the ability (unused) + * @param passive - unused + * @param _simulated - Whether the ability is being simulated (unused) + * @param _stat - The type of the checked {@linkcode Stat} (unused) + * @param statValue - {@linkcode Utils.NumberHolder} containing the value of the checked stat + * @param _checkedPokemon - The {@linkcode Pokemon} this ability is targeting (unused) + * @param _ignoreAbility - Whether the ability should be ignored if possible + * @param _args - unused + * @returns `true` if this changed the checked stat, `false` otherwise. + */ + applyAllyStat(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _stat: BattleStat, statValue: Utils.NumberHolder, _checkedPokemon: Pokemon, _ignoreAbility: boolean, _args: any[]) { + statValue.value *= this.multiplier; + } + + /** + * Check if this ability can apply to the checked stat. + * @param pokemon - The ally {@linkcode Pokemon} with the ability (unused) + * @param passive - unused + * @param simulated - Whether the ability is being simulated (unused) + * @param stat - The type of the checked {@linkcode Stat} + * @param statValue - {@linkcode Utils.NumberHolder} containing the value of the checked stat + * @param checkedPokemon - The {@linkcode Pokemon} this ability is targeting (unused) + * @param ignoreAbility - Whether the ability should be ignored if possible + * @param args - unused + * @returns `true` if this can apply to the checked stat, `false` otherwise. + */ + canApplyAllyStat(pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, ignoreAbility: boolean, args: any[]): boolean { + return stat === this.stat && !(ignoreAbility && this.ignorable); + } +} + /** * Ability attribute for Gorilla Tactics * @extends PostAttackAbAttr @@ -5594,6 +5650,30 @@ export function applyStatMultiplierAbAttrs( args, ); } + +/** + * Applies an ally's Stat multiplier attribute + * @param attrType - {@linkcode AllyStatMultiplierAbAttr} should always be AllyStatMultiplierAbAttr for the time being + * @param pokemon - The {@linkcode Pokemon} with the ability + * @param stat - The type of the checked {@linkcode Stat} + * @param statValue - {@linkcode Utils.NumberHolder} containing the value of the checked stat + * @param checkedPokemon - The {@linkcode Pokemon} with the checked stat + * @param ignoreAbility - Whether or not the ability should be ignored by the pokemon or its move. + * @param args - unused + */ +export function applyAllyStatMultiplierAbAttrs(attrType: Constructor, + pokemon: Pokemon, stat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, checkedPokemon: Pokemon, ignoreAbility: boolean, ...args: any[] +): void { + return applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => attr.applyAllyStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, ignoreAbility, args), + (attr, passive) => attr.canApplyAllyStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, ignoreAbility, args), + args, + simulated, + ); +} + export function applyPostSetStatusAbAttrs( attrType: Constructor, pokemon: Pokemon, @@ -5606,7 +5686,8 @@ export function applyPostSetStatusAbAttrs( attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), - (attr, passive) => attr.canApplyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, + (attr, passive) => attr.canApplyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), + args, simulated, ); } @@ -6437,11 +6518,12 @@ export function initAbilities() { new Ability(Abilities.FLOWER_GIFT, 4) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 1.5) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.SPDEF, 1.5) + .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), AllyStatMultiplierAbAttr, Stat.ATK, 1.5) + .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), AllyStatMultiplierAbAttr, Stat.SPDEF, 1.5) .attr(UncopiableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .attr(PostSummonFormChangeByWeatherAbAttr, Abilities.FLOWER_GIFT) .attr(PostWeatherChangeFormChangeAbAttr, Abilities.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]) - .partial() // Should also boosts stats of ally .ignorable(), new Ability(Abilities.BAD_DREAMS, 4) .attr(PostTurnHurtIfSleepingAbAttr), @@ -6577,7 +6659,7 @@ export function initAbilities() { .bypassFaint(), new Ability(Abilities.VICTORY_STAR, 5) .attr(StatMultiplierAbAttr, Stat.ACC, 1.1) - .partial(), // Does not boost ally's accuracy + .attr(AllyStatMultiplierAbAttr, Stat.ACC, 1.1, false), new Ability(Abilities.TURBOBLAZE, 5) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTurboblaze", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(MoveAbilityBypassAbAttr), diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c4004e9c582..8981644d885 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1644,7 +1644,9 @@ export class ContactDamageProtectedTag extends ProtectedTag { if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { const attacker = effectPhase.getPokemon(); if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { - attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT }); + attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { + result: HitResult.INDIRECT, + }); } } } @@ -1970,7 +1972,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { let highestStat: EffectiveStat; EFFECTIVE_STATS.map(s => - pokemon.getEffectiveStat(s, undefined, undefined, undefined, undefined, undefined, undefined, true), + pokemon.getEffectiveStat(s, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true), ).reduce((highestValue: number, value: number, i: number) => { if (value > highestValue) { highestStat = EFFECTIVE_STATS[i]; @@ -2240,7 +2242,9 @@ export class SaltCuredTag extends BattlerTag { if (!cancelled.value) { const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER); - pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); + pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8), { + result: HitResult.INDIRECT, + }); globalScene.queueMessage( i18next.t("battlerTags:saltCuredLapse", { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index c85ec7db295..e18e898bc68 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -4798,8 +4798,8 @@ export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.NumberHolder); - const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true); - const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true); + const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true, true, true); + const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true, true, true); if (predictedPhysDmg > predictedSpecDmg) { category.value = MoveCategory.PHYSICAL; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a7532685bea..51a5a10b010 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -192,6 +192,9 @@ import { applyPreLeaveFieldAbAttrs, applyOnLoseAbAttrs, PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, + applyAllyStatMultiplierAbAttrs, + AllyStatMultiplierAbAttr, + MoveAbilityBypassAbAttr, } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; @@ -260,6 +263,7 @@ import { import { Nature } from "#enums/nature"; import { StatusEffect } from "#enums/status-effect"; import { doShinySparkleAnim } from "#app/field/anims"; +import { MoveFlags } from "#enums/MoveFlags"; export enum LearnMoveSituation { MISC, @@ -1389,6 +1393,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param move the {@linkcode Move} being used * @param ignoreAbility determines whether this Pokemon's abilities should be ignored during the stat calculation * @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation. + * @param ignoreAllyAbility during an attack, determines whether the ally Pokemon's abilities should be ignored during the stat calculation. * @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` @@ -1400,6 +1405,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { move?: Move, ignoreAbility = false, ignoreOppAbility = false, + ignoreAllyAbility = false, isCritical = false, simulated = true, ignoreHeldItems = false, @@ -1441,6 +1447,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ); } + const ally = this.getAlly(); + if (ally) { + applyAllyStatMultiplierAbAttrs(AllyStatMultiplierAbAttr, ally, stat, statValue, simulated, this, move?.hasFlag(MoveFlags.IGNORE_ABILITIES) || ignoreAllyAbility); + } + let ret = statValue.value * this.getStatStageMultiplier( @@ -3889,6 +3900,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { evasionMultiplier, ); + const ally = this.getAlly(); + if (ally) { + const ignore = this.hasAbilityWithAttr(MoveAbilityBypassAbAttr) || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES); + applyAllyStatMultiplierAbAttrs(AllyStatMultiplierAbAttr, ally, Stat.ACC, accuracyMultiplier, false, this, ignore); + applyAllyStatMultiplierAbAttrs(AllyStatMultiplierAbAttr, ally, Stat.EVA, evasionMultiplier, false, this, ignore); + } + return accuracyMultiplier.value / evasionMultiplier.value; } @@ -3900,6 +3918,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @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 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 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. @@ -3910,6 +3930,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { moveCategory: MoveCategory, ignoreAbility = false, ignoreSourceAbility = false, + ignoreAllyAbility = false, + ignoreSourceAllyAbility = false, isCritical = false, simulated = true, ): number { @@ -3932,6 +3954,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { undefined, ignoreSourceAbility, ignoreAbility, + ignoreAllyAbility, isCritical, simulated, ), @@ -3949,6 +3972,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { move, ignoreAbility, ignoreSourceAbility, + ignoreSourceAllyAbility, isCritical, simulated, ), @@ -3983,6 +4007,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @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 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 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: @@ -3995,6 +4021,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { move: Move, ignoreAbility = false, ignoreSourceAbility = false, + ignoreAllyAbility = false, + ignoreSourceAllyAbility = false, isCritical = false, simulated = true, ): DamageCalculationResult { @@ -4104,6 +4132,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { moveCategory, ignoreAbility, ignoreSourceAbility, + ignoreAllyAbility, + ignoreSourceAllyAbility, isCritical, simulated, ); @@ -4429,7 +4459,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { cancelled, result, damage: dmg, - } = this.getAttackDamage(source, move, false, false, isCritical, false); + } = this.getAttackDamage(source, move, false, false, false, false, isCritical, false); const typeBoost = source.findTag( t => @@ -7114,6 +7144,8 @@ export class EnemyPokemon extends Pokemon { move, !p.battleData.abilityRevealed, false, + !p.getAlly()?.battleData.abilityRevealed, + false, isCritical, ).damage >= p.hp ); diff --git a/test/abilities/flower_gift.test.ts b/test/abilities/flower_gift.test.ts index fff509a1f00..1104a3c111f 100644 --- a/test/abilities/flower_gift.test.ts +++ b/test/abilities/flower_gift.test.ts @@ -1,12 +1,14 @@ import { BattlerIndex } from "#app/battle"; +import { allAbilities } from "#app/data/ability"; import { Abilities } from "#app/enums/abilities"; import { Stat } from "#app/enums/stat"; import { WeatherType } from "#app/enums/weather-type"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Abilities - Flower Gift", () => { let phaserGame: Phaser.Game; @@ -28,6 +30,59 @@ describe("Abilities - Flower Gift", () => { expect(game.scene.getPlayerPokemon()?.formIndex).toBe(OVERCAST_FORM); }; + /** + * Tests damage dealt by a move used against a target before and after Flower Gift is activated. + * @param game The game manager instance + * @param move The move that should be used + * @param allyAttacker True if the ally is attacking the enemy, false if the enemy is attacking the ally + * @param ability The ability that the ally pokemon should have + * @param enemyAbility The ability that the enemy pokemon should have + * + * @returns Two numbers, the first being the damage done to the target without flower gift active, the second being the damage done with flower gift active + */ + const testDamageDealt = async (game: GameManager, move: Moves, allyAttacker: boolean, allyAbility = Abilities.BALL_FETCH, enemyAbility = Abilities.BALL_FETCH): Promise<[number, number]> => { + game.override.battleType("double"); + game.override.moveset([ Moves.SPLASH, Moves.SUNNY_DAY, move, Moves.HEAL_PULSE ]); + game.override.enemyMoveset([ Moves.SPLASH, Moves.HEAL_PULSE ]); + const target_index = allyAttacker ? BattlerIndex.ENEMY : BattlerIndex.PLAYER_2; + const attacker_index = allyAttacker ? BattlerIndex.PLAYER_2 : BattlerIndex.ENEMY; + const ally_move = allyAttacker ? move : Moves.SPLASH; + const enemy_move = allyAttacker ? Moves.SPLASH : move; + const ally_target = allyAttacker ? BattlerIndex.ENEMY : null; + + await game.classicMode.startBattle([ Species.CHERRIM, Species.MAGIKARP ]); + const target = allyAttacker ? game.scene.getEnemyField()[0] : game.scene.getPlayerField()[1]; + const initialHp = target.getMaxHp(); + + // Override the ability for the target and attacker only + vi.spyOn(game.scene.getPlayerField()[1], "getAbility").mockReturnValue(allAbilities[allyAbility]); + vi.spyOn(game.scene.getEnemyField()[0], "getAbility").mockReturnValue(allAbilities[enemyAbility]); + + // turn 1 + game.move.select(Moves.SUNNY_DAY, 0); + game.move.select(ally_move, 1, ally_target); + await game.forceEnemyMove(enemy_move, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + // Ensure sunny day is used last. + await game.setTurnOrder([ attacker_index, target_index, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to(TurnEndPhase); + const damageWithoutGift = initialHp - target.hp; + + target.hp = initialHp; + + // turn 2. Make target use recover to reset hp calculation. + game.move.select(Moves.SPLASH, 0, target_index); + game.move.select(ally_move, 1, ally_target); + await game.forceEnemyMove(enemy_move, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, target_index, attacker_index ]); + await game.phaseInterceptor.to(TurnEndPhase); + const damageWithGift = initialHp - target.hp; + + return [ damageWithoutGift, damageWithGift ]; + }; + + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -41,23 +96,24 @@ describe("Abilities - Flower Gift", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([Moves.SPLASH, Moves.RAIN_DANCE, Moves.SUNNY_DAY, Moves.SKILL_SWAP]) + .moveset([Moves.SPLASH, Moves.SUNSTEEL_STRIKE, Moves.SUNNY_DAY, Moves.MUD_SLAP]) .enemySpecies(Species.MAGIKARP) .enemyMoveset(Moves.SPLASH) - .enemyAbility(Abilities.BALL_FETCH); + .enemyAbility(Abilities.BALL_FETCH) + .enemyLevel(100) + .startingLevel(100); }); - // TODO: Uncomment expect statements when the ability is implemented - currently does not increase stats of allies it("increases the ATK and SPDEF stat stages of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => { game.override.battleType("double"); await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]); - const [cherrim] = game.scene.getPlayerField(); + const [cherrim, magikarp] = game.scene.getPlayerField(); const cherrimAtkStat = cherrim.getEffectiveStat(Stat.ATK); const cherrimSpDefStat = cherrim.getEffectiveStat(Stat.SPDEF); - // const magikarpAtkStat = magikarp.getEffectiveStat(Stat.ATK);; - // const magikarpSpDefStat = magikarp.getEffectiveStat(Stat.SPDEF); + const magikarpAtkStat = magikarp.getEffectiveStat(Stat.ATK); + const magikarpSpDefStat = magikarp.getEffectiveStat(Stat.SPDEF); game.move.select(Moves.SUNNY_DAY, 0); game.move.select(Moves.SPLASH, 1); @@ -68,8 +124,28 @@ describe("Abilities - Flower Gift", () => { expect(cherrim.formIndex).toBe(SUNSHINE_FORM); expect(cherrim.getEffectiveStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5)); expect(cherrim.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5)); - // expect(magikarp.getEffectiveStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5)); - // expect(magikarp.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5)); + expect(magikarp.getEffectiveStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5)); + expect(magikarp.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5)); + }); + + it("should not increase the damage of an ally using an ability ignoring move", async () => { + const [ damageWithGift, damageWithoutGift ] = await testDamageDealt(game, Moves.SUNSTEEL_STRIKE, true); + expect(damageWithGift).toBe(damageWithoutGift); + }); + + it("should not increase the damage of a mold breaker ally", async () => { + const [ damageWithGift, damageWithoutGift ] = await testDamageDealt(game, Moves.TACKLE, true, Abilities.MOLD_BREAKER); + expect(damageWithGift).toBe(damageWithoutGift); + }); + + it("should decrease the damage an ally takes from a special attack", async () => { + const [ damageWithoutGift, damageWithGift ] = await testDamageDealt(game, Moves.MUD_SLAP, false); + expect(damageWithGift).toBeLessThan(damageWithoutGift); + }); + + it("should not decrease the damage an ally takes from a mold breaker enemy using a special attack", async () => { + const [ damageWithoutGift, damageWithGift ] = await testDamageDealt(game, Moves.MUD_SLAP, false, Abilities.BALL_FETCH, Abilities.MOLD_BREAKER); + expect(damageWithGift).toBe(damageWithoutGift); }); it("changes the Pokemon's form during Harsh Sunlight", async () => { @@ -92,6 +168,7 @@ describe("Abilities - Flower Gift", () => { it("reverts to Overcast Form when the Pokémon loses Flower Gift, changes form under Harsh Sunlight/Sunny when it regains it", async () => { game.override.enemyMoveset([Moves.SKILL_SWAP]).weather(WeatherType.HARSH_SUN); + game.override.moveset([ Moves.SKILL_SWAP ]); await game.classicMode.startBattle([Species.CHERRIM]); diff --git a/test/abilities/protosynthesis.test.ts b/test/abilities/protosynthesis.test.ts index d0ae46cd951..882474b7cef 100644 --- a/test/abilities/protosynthesis.test.ts +++ b/test/abilities/protosynthesis.test.ts @@ -46,16 +46,56 @@ describe("Abilities - Protosynthesis", () => { // Nature of starting mon is randomized. We need to fix it to a neutral nature for the automated test. mew.setNature(Nature.HARDY); const enemy = game.scene.getEnemyPokemon()!; - const def_before_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true); - const atk_before_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true); + const def_before_boost = mew.getEffectiveStat( + Stat.DEF, + undefined, + undefined, + false, + undefined, + undefined, + false, + false, + true, + ); + const atk_before_boost = mew.getEffectiveStat( + Stat.ATK, + undefined, + undefined, + false, + undefined, + undefined, + false, + false, + true, + ); const initialHp = enemy.hp; game.move.select(Moves.TACKLE); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); const unboosted_dmg = initialHp - enemy.hp; enemy.hp = initialHp; - const def_after_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true); - const atk_after_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true); + const def_after_boost = mew.getEffectiveStat( + Stat.DEF, + undefined, + undefined, + false, + undefined, + undefined, + false, + false, + true, + ); + const atk_after_boost = mew.getEffectiveStat( + Stat.ATK, + undefined, + undefined, + false, + undefined, + undefined, + false, + false, + true, + ); game.move.select(Moves.TACKLE); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); diff --git a/test/abilities/victory_star.test.ts b/test/abilities/victory_star.test.ts new file mode 100644 index 00000000000..456f8cd7ddd --- /dev/null +++ b/test/abilities/victory_star.test.ts @@ -0,0 +1,60 @@ +import { BattlerIndex } from "#app/battle"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Victory Star", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.TACKLE, Moves.SPLASH ]) + .battleType("double") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should increase the accuracy of its user", async () => { + await game.classicMode.startBattle([ Species.VICTINI, Species.MAGIKARP ]); + + const user = game.scene.getPlayerField()[0]; + + vi.spyOn(user, "getAccuracyMultiplier"); + game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(user.getAccuracyMultiplier).toHaveReturnedWith(1.1); + }); + + it("should increase the accuracy of its user's ally", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP, Species.VICTINI ]); + + const ally = game.scene.getPlayerField()[0]; + vi.spyOn(ally, "getAccuracyMultiplier"); + + game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(ally.getAccuracyMultiplier).toHaveReturnedWith(1.1); + }); +});