[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>
This commit is contained in:
Sirz Benjie 2025-03-28 22:24:19 -05:00 committed by GitHub
parent 4a560d7185
commit fac20ca97a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 317 additions and 22 deletions

View File

@ -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<AllyStatMultiplierAbAttr>,
pokemon: Pokemon, stat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, checkedPokemon: Pokemon, ignoreAbility: boolean, ...args: any[]
): void {
return applyAbAttrsInternal<AllyStatMultiplierAbAttr>(
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<PostSetStatusAbAttr>,
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),

View File

@ -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", {

View File

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

View File

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

View File

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

View File

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

View File

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