diff --git a/src/data/ability.ts b/src/data/ability.ts index c49d25ed4a8..0f87e7167d1 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -13,7 +13,7 @@ import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, RecoilAttr, Stat import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagType } from "./enums/arena-tag-type"; import { Stat } from "./pokemon-stat"; -import { PokemonHeldItemModifier } from "../modifier/modifier"; +import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { Moves } from "./enums/moves"; import { TerrainType } from "./terrain"; import { SpeciesFormChangeManualTrigger } from "./pokemon-forms"; @@ -23,6 +23,7 @@ import { Command } from "../ui/command-ui-handler"; import Battle from "#app/battle.js"; import { ability } from "#app/locales/en/ability.js"; import { PokeballType, getPokeballName } from "./pokeball"; +import { BerryModifierType } from "#app/modifier/modifier-type"; export class Ability implements Localizable { public id: Abilities; @@ -127,7 +128,7 @@ export abstract class AbAttr { return null; } - getCondition(): AbAttrCondition { + getCondition(): AbAttrCondition | null { return this.extraCondition || null; } @@ -2228,6 +2229,71 @@ export class PostTurnResetStatusAbAttr extends PostTurnAbAttr { } } +/** + * After the turn ends, try to create an extra item + */ +export class PostTurnLootAbAttr extends PostTurnAbAttr { + /** + * @param itemType - The type of item to create + * @param procChance - Chance to create an item + * @see {@linkcode applyPostTurn()} + */ + constructor( + /** Extend itemType to add more options */ + private itemType: "EATEN_BERRIES" | "HELD_BERRIES", + private procChance: (pokemon: Pokemon) => number + ) { + super(); + } + + applyPostTurn(pokemon: Pokemon, passive: boolean, args: any[]): boolean { + const pass = Phaser.Math.RND.realInRange(0, 1); + // Clamp procChance to [0, 1]. Skip if didn't proc (less than pass) + if (Math.max(Math.min(this.procChance(pokemon), 1), 0) < pass) { + return false; + } + + if (this.itemType === "EATEN_BERRIES") { + return this.createEatenBerry(pokemon); + } else { + return false; + } + } + + /** + * Create a new berry chosen randomly from the berries the pokemon ate this battle + * @param pokemon The pokemon with this ability + * @returns whether a new berry was created + */ + createEatenBerry(pokemon: Pokemon): boolean { + const berriesEaten = pokemon.battleData.berriesEaten; + + if (!berriesEaten.length) { + return false; + } + + const randomIdx = Utils.randSeedInt(berriesEaten.length); + const chosenBerry = new BerryModifierType(berriesEaten[randomIdx]); + berriesEaten.splice(randomIdx) // Remove berry from memory + + const berryModifier = pokemon.scene.findModifier( + (m) => m instanceof BerryModifier && m.berryType === berriesEaten[randomIdx], + pokemon.isPlayer() + ) as BerryModifier | undefined; + + if (!berryModifier) { + pokemon.scene.addModifier(new BerryModifier(chosenBerry, pokemon.id, berriesEaten[randomIdx], 1)); + } else { + berryModifier.stackCount++; + } + + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` harvested one ${chosenBerry.name}!`)); + pokemon.scene.updateModifiers(pokemon.isPlayer()); + + return true; + } +} + export class MoodyAbAttr extends PostTurnAbAttr { constructor() { super(true); @@ -3418,7 +3484,13 @@ export function initAbilities() { new Ability(Abilities.FLARE_BOOST, 5) .attr(MovePowerBoostAbAttr, (user, target, move) => move.category === MoveCategory.SPECIAL && user.status?.effect === StatusEffect.BURN, 1.5), new Ability(Abilities.HARVEST, 5) - .unimplemented(), + .attr( + PostTurnLootAbAttr, + "EATEN_BERRIES", + /** Rate is doubled when under sun {@link https://dex.pokemonshowdown.com/abilities/harvest} */ + (pokemon) => 0.5 * (getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)(pokemon) ? 2 : 1) + ) + .partial(), new Ability(Abilities.TELEPATHY, 5) .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon.getAlly() === attacker && move.getMove() instanceof AttackMove) .ignorable(), diff --git a/src/phases.ts b/src/phases.ts index 1d56261d10e..d1dacee6f24 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2076,6 +2076,8 @@ export class TurnStartPhase extends FieldPhase { } } + this.scene.pushPhase(new BerryPhase(this.scene)); + if (this.scene.arena.weather) this.scene.pushPhase(new WeatherEffectPhase(this.scene, this.scene.arena.weather)); @@ -2090,6 +2092,42 @@ export class TurnStartPhase extends FieldPhase { } } +/** The phase after attacks where the pokemon eat berries */ +export class BerryPhase extends FieldPhase { + start() { + super.start(); + + this.executeForAll((pokemon) => { + const hasUsableBerry = !!this.scene.findModifier((m) => m instanceof BerryModifier && m.shouldApply([pokemon]), pokemon.isPlayer()); + + if (hasUsableBerry) { + const cancelled = new Utils.BooleanHolder(false); + pokemon.getOpponents().map((opp) => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled)); + + if (cancelled.value) { + pokemon.scene.queueMessage(getPokemonMessage(pokemon, " is too\nnervous to eat berries!")); + } else { + this.scene.unshiftPhase(new CommonAnimPhase(this.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM)); + + for (const berryModifier of this.scene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon) as BerryModifier[]) { + if (berryModifier.consumed) { + if (!--berryModifier.stackCount) { + this.scene.removeModifier(berryModifier); + } else { + berryModifier.consumed = false; + } + } + } + + this.scene.updateModifiers(pokemon.isPlayer()); + } + } + }); + + this.end(); + } +} + export class TurnEndPhase extends FieldPhase { constructor(scene: BattleScene) { super(scene); @@ -2108,10 +2146,6 @@ export class TurnEndPhase extends FieldPhase { pokemon.summonData.disabledMove = Moves.NONE; } - const hasUsableBerry = !!this.scene.findModifier(m => m instanceof BerryModifier && m.shouldApply([ pokemon ]), pokemon.isPlayer()); - if (hasUsableBerry) - this.scene.unshiftPhase(new BerryPhase(this.scene, pokemon.getBattlerIndex())); - this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { @@ -4093,38 +4127,6 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { } } -export class BerryPhase extends CommonAnimPhase { - constructor(scene: BattleScene, battlerIndex: BattlerIndex) { - super(scene, battlerIndex, undefined, CommonAnim.USE_ITEM); - } - - start() { - let berryModifiers: BerryModifier[]; - - const pokemon = this.getPokemon(); - - const cancelled = new Utils.BooleanHolder(false); - pokemon.getOpponents().map(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled)); - - if (cancelled.value) - pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is too\nnervous to eat berries!')); - else if ((berryModifiers = this.scene.applyModifiers(BerryModifier, this.player, pokemon) as BerryModifier[])) { - for (let berryModifier of berryModifiers) { - if (berryModifier.consumed) { - if (!--berryModifier.stackCount) - this.scene.removeModifier(berryModifier); - else - berryModifier.consumed = false; - this.scene.updateModifiers(this.player); - } - } - return super.start(); - } - - this.end(); - } -} - export class PokemonHealPhase extends CommonAnimPhase { private hpHealed: integer; private message: string;