Skip to content

Commit

Permalink
Implementation of natural gift
Browse files Browse the repository at this point in the history
  • Loading branch information
geeilhan committed Jan 22, 2025
1 parent 356cffa commit 677f15c
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 14 deletions.
91 changes: 86 additions & 5 deletions src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4644,48 +4644,125 @@ 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.
* @param args - {@linkcode Utils.NumberHolder} the power of user's move
* @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.
* @param args - {@linkcode Utils.NumberHolder} the move type
* @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
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 1 addition & 8 deletions src/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<InstanceType<typeof DefaultOverrides>>;
const overrides = {} satisfies Partial<InstanceType<typeof DefaultOverrides>>;

/**
* If you need to add Overrides values for local testing do that inside {@linkcode overrides}
Expand Down
162 changes: 162 additions & 0 deletions src/test/moves/natural_gift.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 677f15c

Please sign in to comment.