diff --git a/public/locales b/public/locales index acad8499a4c..e07ab625f20 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit acad8499a4ca488a9871902de140f635235f309a +Subproject commit e07ab625f2080afe36b61fad291b0ec5eff4000c diff --git a/src/data/move.ts b/src/data/move.ts index a3e909dc923..980d9fa11ee 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4644,14 +4644,17 @@ export class VariableMoveTypeAttr extends MoveAttr { } /** - * Attribute used to control the Power and Type of Natural Gift + * Attribute used to control the Power and Type of Natural Gift. + * Takes over the Power calculation of Natural Gift while {@linkcode NaturalGiftTypeAttr} + * takes care of the Move Type. * @extends VariablePowerAttr */ export class NaturalGiftPowerAttr extends VariablePowerAttr { private randomBerry; /** - * Overrides the power of Natural Gift depending on the consumed berry + * Overrides the power of Natural Gift depending on the consumed berry. + * This also removes the berry. * @param user - The Pokémon using the move. * @param target - The target Pokémon. * @param move - The move being used. @@ -4659,20 +4662,56 @@ export class NaturalGiftPowerAttr extends VariablePowerAttr { * @returns A boolean indicating whether the move was successfully applied. */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - console.log("GHNote Natural Gift POWER Attr called"); + const power = args[0]; + if (!(power instanceof Utils.NumberHolder)) { + return false; + } + + this.randomBerry = NaturalGiftBerrySelector.getRandomBerry(user); + + if (this.randomBerry) { + power.value = this.randomBerry.getNaturalGiftPower(); + + /** Berries do not get eaten during specific weather conditions */ + const weather = globalScene.arena.weather; + if (!weather?.isEffectSuppressed()) { + if (weather?.weatherType === WeatherType.HEAVY_RAIN && this.randomBerry.getNaturalGiftType === Type.FIRE) { + NaturalGiftBerrySelector.resetBerry(); + return true; + } else if (weather?.weatherType === WeatherType.HARSH_SUN && this.randomBerry.getNaturalGiftType === Type.WATER) { + NaturalGiftBerrySelector.resetBerry(); + return true; + } + } + /** If user used {@linkcode Moves.POWDER} and Natural Gift turns fire type the berry is not confused*/ + if (user.getTag(BattlerTagType.POWDER) && this.randomBerry.getNaturalGiftType === Type.FIRE) { + NaturalGiftBerrySelector.resetBerry(); + return true; + } + + user.loseHeldItem(this.randomBerry, user.isPlayer()); + globalScene.updateModifiers(user.isPlayer()); + NaturalGiftBerrySelector.resetBerry(); + + return true; + } return false; } } /** * Attribute used to control the type of Natural Gift + * Takes over the Type calculation of Natural Gift while {@linkcode NaturalGiftPowerAttr} + * takes care of the Move power. * @extends VariableMoveTypeAttr */ export class NaturalGiftTypeAttr extends VariableMoveTypeAttr { private randomBerry; /** - * Overrides the type of Natural Gift depending on the consumed berry + * Overrides the type of Natural Gift depending on the consumed berry. + * The item used for the move is not removed here but in {@linkcode NaturalGiftPowerAttr} + * since it is called last. * @param user - The Pokémon using the move. * @param target - The target Pokémon. * @param move - The move being used. @@ -4680,12 +4719,50 @@ export class NaturalGiftTypeAttr extends VariableMoveTypeAttr { * @returns A boolean indicating whether the move was successfully applied. */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - console.log("GHNote Natural Gift TYPE Attr called"); + const moveType = args[0]; + if (!(moveType instanceof Utils.NumberHolder)) { + return false; + } + + this.randomBerry = NaturalGiftBerrySelector.getRandomBerry(user); + + if (this.randomBerry) { + moveType.value = this.randomBerry.getNaturalGiftType(); + return true; + } return false; } } +class NaturalGiftBerrySelector { + private static selectedBerry; + + /** + * select random berry from user + * @param user Pokemon using Natural Gift + * @returns A random berry to use in {@linkcode NaturalGiftPowerAttr} and {@linkcode NaturalGiftTypeAttr} + */ + public static getRandomBerry(user: Pokemon): BerryModifier { + if (!this.selectedBerry) { + const berries = globalScene.findModifiers( + m => m instanceof BerryModifier && m.pokemonId === user.id, + user.isPlayer() + ) as BerryModifier[]; + this.selectedBerry = berries.at(user.randSeedInt(berries.length)) ?? null; + } + return this.selectedBerry; + } + + /** + * Reset the selected berry + */ + public static resetBerry() { + this.selectedBerry = undefined; + } +} + + /** * Attribute used for Tera Starstorm that changes the move type to Stellar * @extends VariableMoveTypeAttr @@ -9362,6 +9439,10 @@ export function initMoves() { new AttackMove(Moves.NATURAL_GIFT, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 4) .attr(NaturalGiftPowerAttr) .attr(NaturalGiftTypeAttr) + .condition((user) => { + const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()); + return userBerries.length > 0; + }) .makesContact(false), //.unimplemented(), new AttackMove(Moves.FEINT, Type.NORMAL, MoveCategory.PHYSICAL, 30, 100, 10, -1, 2, 4) diff --git a/src/overrides.ts b/src/overrides.ts index da8e00f9246..1f8601b7659 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -16,7 +16,6 @@ import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; -import { BerryType } from "#enums/berry-type"; /** * Overrides that are using when testing different in game situations @@ -33,13 +32,7 @@ import { BerryType } from "#enums/berry-type"; * } * ``` */ -const overrides = { - OPP_LEVEL_OVERRIDE: 100, - OPP_MOVESET_OVERRIDE: [ Moves.SPLASH ], - STARTING_LEVEL_OVERRIDE: 1, - MOVESET_OVERRIDE: [ Moves.NATURAL_GIFT ], - STARTING_HELD_ITEMS_OVERRIDE: [{ name: "BERRY", type: BerryType.SITRUS }] -} satisfies Partial>; +const overrides = {} satisfies Partial>; /** * If you need to add Overrides values for local testing do that inside {@linkcode overrides} diff --git a/src/test/moves/natural_gift.test.ts b/src/test/moves/natural_gift.test.ts new file mode 100644 index 00000000000..e1f03280aa5 --- /dev/null +++ b/src/test/moves/natural_gift.test.ts @@ -0,0 +1,162 @@ +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { MoveResult } from "#app/field/pokemon"; +import type Pokemon from "#app/field/pokemon"; +import { BerryType } from "#enums/berry-type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Natural Gift", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + /** + * Count the number of held items a Pokemon has, accounting for stacks of multiple items. + */ + function getHeldItemCount(pokemon: Pokemon): number { + const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount()); + if (stackCounts.length) { + return stackCounts.reduce((a, b) => a + b); + } else { + return 0; + } + } + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .disableCrits() + .moveset(Moves.NATURAL_GIFT) + .ability(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH) + .enemySpecies(Species.RATTATA) + .enemyMoveset(Moves.SPLASH) + .startingLevel(10) + .enemyLevel(100); + }); + + /** + * There is currently no way to test interaction with heavy rain(Weather), harsh sunlight(Weather) and Powder(Move) + * since there are currently no berries that change the type to Fire/Water + */ + it("should deal double damage to Fighting type if Sitrus Berry is consumed", async () => { + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 3 }, + ]) + .enemySpecies(Species.MACHAMP); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.NATURAL_GIFT); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + expect(getHeldItemCount(player)).toBe(2); + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); + }); + + it("should deal half damage to Steel type if Sitrus Berry is consumed", async () => { + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 3 }, + ]) + .enemySpecies(Species.KLINK); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.NATURAL_GIFT); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + expect(getHeldItemCount(player)).toBe(2); + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5); + }); + + /** + * Ganlon Berry should turn Natural Gift to Ice type (1/2x dmg to Water type). + * With Electrify Natural Gift should deal 2x dmg to Water type + */ + it("should not override Electrify (deal double damage against Water pkm with Ganlon Berry)", async () => { + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.GANLON, count: 3 }, + ]) + .enemyMoveset(Moves.ELECTRIFY) + .enemySpecies(Species.MAGIKARP); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.NATURAL_GIFT); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(getHeldItemCount(player)).toBe(2); + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); + }); + + it("should fail if no berries are held", async () => { + game.override + .startingHeldItems([]); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.NATURAL_GIFT); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + }); + + it("should not be affected by Normalize", async () => { + game.override + .startingHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 3 }, + ]) + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.MACHAMP); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.NATURAL_GIFT); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + expect(getHeldItemCount(player)).toBe(2); + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); + }); +});