diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 40818a8eaaf..4be9247f18c 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -53,7 +53,7 @@ import { allMoves } from "./data/data-lists"; import { MusicPreference } from "#app/system/settings/settings"; import { getDefaultModifierTypeForTier, - getEnemyModifierTypesForWave, + getEnemyHeldItemsForWave, getLuckString, getLuckTextTint, getModifierPoolForType, @@ -3223,13 +3223,13 @@ export default class BattleScene extends SceneBase { if (isBoss) { count = Math.max(count, Math.floor(chances / 2)); } - getEnemyModifierTypesForWave( + getEnemyHeldItemsForWave( difficultyWaveIndex, count, [enemyPokemon], this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, upgradeChance, - ).map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false)); + ).map(itemId => enemyPokemon.heldItemManager.add(itemId)); } return true; }); diff --git a/src/enums/held-items.ts b/src/enums/held-items.ts index 7871eeedc74..727dc4f7f0e 100644 --- a/src/enums/held-items.ts +++ b/src/enums/held-items.ts @@ -71,6 +71,17 @@ export const HeldItems = { FLAME_ORB: 0x070C, SOUL_DEW: 0x070D, BATON: 0x070E, + + // Mini Black Hole + MINI_BLACK_HOLE: 0x0801, + + // Vitamins + HP_UP: 0x0901, + PROTEIN: 0x0902, + IRON: 0x0903, + CALCIUM: 0x0904, + ZINC: 0x0905, + CARBOS: 0x0906, }; export type HeldItems = (typeof HeldItems)[keyof typeof HeldItems]; diff --git a/src/items/all-held-items.ts b/src/items/all-held-items.ts index ec054f493a5..273cf8221b8 100644 --- a/src/items/all-held-items.ts +++ b/src/items/all-held-items.ts @@ -1,11 +1,20 @@ +import { getEnumValues } from "#app/utils/common"; +import { BerryType } from "#enums/berry-type"; import { HeldItems } from "#enums/held-items"; import type { PokemonType } from "#enums/pokemon-type"; +import type { PermanentStat } from "#enums/stat"; import { ITEM_EFFECT } from "./held-item"; import { type ATTACK_TYPE_BOOST_PARAMS, AttackTypeBoosterHeldItem, attackTypeToHeldItem, } from "./held-items/attack-type-booster"; +import { + type BASE_STAT_BOOSTER_PARAMS, + BaseStatBoosterHeldItem, + permanentStatToHeldItem, +} from "./held-items/base-stat-booster"; +import { type BERRY_PARAMS, BerryHeldItem, berryTypeToHeldItem } from "./held-items/berry"; import { type EXP_BOOST_PARAMS, ExpBoosterHeldItem } from "./held-items/exp-booster"; import { type HIT_HEAL_PARAMS, HitHealHeldItem } from "./held-items/hit-heal"; import type { RESET_NEGATIVE_STAT_STAGE_PARAMS } from "./held-items/reset-negative-stat-stage"; @@ -20,12 +29,29 @@ export function initHeldItems() { const pokemonType = Number(typeKey) as PokemonType; allHeldItems[heldItemType] = new AttackTypeBoosterHeldItem(heldItemType, 99, pokemonType, 0.2); } + + // vitamins + for (const [statKey, heldItemType] of Object.entries(permanentStatToHeldItem)) { + const stat = Number(statKey) as PermanentStat; + allHeldItems[heldItemType] = new BaseStatBoosterHeldItem(heldItemType, 10, stat); + } + allHeldItems[HeldItems.LEFTOVERS] = new TurnEndHealHeldItem(HeldItems.LEFTOVERS, 4); allHeldItems[HeldItems.SHELL_BELL] = new HitHealHeldItem(HeldItems.SHELL_BELL, 4); allHeldItems[HeldItems.LUCKY_EGG] = new ExpBoosterHeldItem(HeldItems.LUCKY_EGG, 99, 40); allHeldItems[HeldItems.GOLDEN_EGG] = new ExpBoosterHeldItem(HeldItems.GOLDEN_EGG, 99, 100); + for (const berry of getEnumValues(BerryType)) { + let maxStackCount: number; + if ([BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(berry)) { + maxStackCount = 2; + } else { + maxStackCount = 3; + } + const berryId = berryTypeToHeldItem[berry]; + allHeldItems[berryId] = new BerryHeldItem(berry, maxStackCount); + } console.log(allHeldItems); } @@ -35,6 +61,8 @@ type APPLY_HELD_ITEMS_PARAMS = { [ITEM_EFFECT.HIT_HEAL]: HIT_HEAL_PARAMS; [ITEM_EFFECT.RESET_NEGATIVE_STAT_STAGE]: RESET_NEGATIVE_STAT_STAGE_PARAMS; [ITEM_EFFECT.EXP_BOOSTER]: EXP_BOOST_PARAMS; + [ITEM_EFFECT.BERRY]: BERRY_PARAMS; + [ITEM_EFFECT.BASE_STAT_BOOSTER]: BASE_STAT_BOOSTER_PARAMS; }; export function applyHeldItems(effect: T, params: APPLY_HELD_ITEMS_PARAMS[T]) { diff --git a/src/items/held-item.ts b/src/items/held-item.ts index 013593a738b..8d1f5137637 100644 --- a/src/items/held-item.ts +++ b/src/items/held-item.ts @@ -1,3 +1,4 @@ +import { applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/abilities/ability"; import type Pokemon from "#app/field/pokemon"; import { globalScene } from "#app/global-scene"; import type { HeldItems } from "#enums/held-items"; @@ -8,6 +9,9 @@ export const ITEM_EFFECT = { HIT_HEAL: 3, RESET_NEGATIVE_STAT_STAGE: 4, EXP_BOOSTER: 5, + // Should we actually distinguish different berry effects? + BERRY: 6, + BASE_STAT_BOOSTER: 7, } as const; export type ITEM_EFFECT = (typeof ITEM_EFFECT)[keyof typeof ITEM_EFFECT]; @@ -41,6 +45,7 @@ export class HeldItem { return ""; } + // TODO: Aren't these fine as just properties to set in the subclass definition? untransferable(): HeldItem { this.isTransferable = false; return this; @@ -119,10 +124,15 @@ export class HeldItem { } export class ConsumableHeldItem extends HeldItem { - consume(pokemon: Pokemon, isPlayer: boolean): boolean { - pokemon.heldItemManager.remove(this.type, 1); - // TODO: Turn this into updateItemBar or something - globalScene.updateModifiers(isPlayer); - return true; + // Sometimes berries are not eaten, some stuff may not proc unburden... + consume(pokemon: Pokemon, isPlayer: boolean, remove = true, unburden = true): void { + if (remove) { + pokemon.heldItemManager.remove(this.type, 1); + // TODO: Turn this into updateItemBar or something + globalScene.updateModifiers(isPlayer); + } + if (unburden) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + } } } diff --git a/src/items/held-items/attack-type-booster.ts b/src/items/held-items/attack-type-booster.ts index 4d1df7af8ff..f9048480e3f 100644 --- a/src/items/held-items/attack-type-booster.ts +++ b/src/items/held-items/attack-type-booster.ts @@ -44,6 +44,7 @@ export class AttackTypeBoosterHeldItem extends HeldItem { public moveType: PokemonType; public powerBoost: number; + // This constructor may need a revision constructor(type: HeldItems, maxStackCount = 1, moveType: PokemonType, powerBoost: number) { super(type, maxStackCount); this.moveType = moveType; diff --git a/src/items/held-items/base-stat-booster.ts b/src/items/held-items/base-stat-booster.ts new file mode 100644 index 00000000000..d65675a35c9 --- /dev/null +++ b/src/items/held-items/base-stat-booster.ts @@ -0,0 +1,81 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItems } from "#enums/held-items"; +import { getStatKey, type PermanentStat, Stat } from "#enums/stat"; +import i18next from "i18next"; +import { HeldItem, ITEM_EFFECT } from "../held-item"; + +export interface BASE_STAT_BOOSTER_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + baseStats: number[]; +} + +interface PermanentStatToHeldItemMap { + [key: number]: HeldItems; +} + +export const permanentStatToHeldItem: PermanentStatToHeldItemMap = { + [Stat.HP]: HeldItems.HP_UP, + [Stat.ATK]: HeldItems.PROTEIN, + [Stat.DEF]: HeldItems.IRON, + [Stat.SPATK]: HeldItems.CALCIUM, + [Stat.SPDEF]: HeldItems.ZINC, + [Stat.SPD]: HeldItems.CARBOS, +}; + +export const statBoostItems: Record = { + [Stat.HP]: "hp_up", + [Stat.ATK]: "protein", + [Stat.DEF]: "iron", + [Stat.SPATK]: "calcium", + [Stat.SPDEF]: "zinc", + [Stat.SPD]: "carbos", +}; + +export class BaseStatBoosterHeldItem extends HeldItem { + public effects: ITEM_EFFECT[] = [ITEM_EFFECT.BASE_STAT_BOOSTER]; + public stat: PermanentStat; + + constructor(type: HeldItems, maxStackCount = 1, stat: PermanentStat) { + super(type, maxStackCount); + this.stat = stat; + } + + get name(): string { + return i18next.t(`modifierType:BaseStatBoosterItem.${statBoostItems[this.stat]}`); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", { + stat: i18next.t(getStatKey(this.stat)), + }); + } + + get iconName(): string { + return statBoostItems[this.stat]; + } + + /** + * Checks if {@linkcode BaseStatModifier} should be applied to the specified {@linkcode Pokemon}. + * @param _pokemon the {@linkcode Pokemon} to be modified + * @param baseStats the base stats of the {@linkcode Pokemon} + * @returns `true` if the {@linkcode Pokemon} should be modified + */ + // override shouldApply(_pokemon?: Pokemon, baseStats?: number[]): boolean { + // return super.shouldApply(_pokemon, baseStats) && Array.isArray(baseStats); + // } + + /** + * Applies the {@linkcode BaseStatModifier} to the specified {@linkcode Pokemon}. + * @param _pokemon the {@linkcode Pokemon} to be modified + * @param baseStats the base stats of the {@linkcode Pokemon} + * @returns always `true` + */ + apply(params: BASE_STAT_BOOSTER_PARAMS): boolean { + const pokemon = params.pokemon; + const stackCount = pokemon.heldItemManager.getStack(this.type); + const baseStats = params.baseStats; + baseStats[this.stat] = Math.floor(baseStats[this.stat] * (1 + stackCount * 0.1)); + return true; + } +} diff --git a/src/items/held-items/berry.ts b/src/items/held-items/berry.ts new file mode 100644 index 00000000000..28f453d664c --- /dev/null +++ b/src/items/held-items/berry.ts @@ -0,0 +1,99 @@ +import { getBerryEffectDescription, getBerryEffectFunc, getBerryName } from "#app/data/berry"; +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { ConsumableHeldItem, ITEM_EFFECT } from "#app/items/held-item"; +import { PreserveBerryModifier } from "#app/modifier/modifier"; +import { BooleanHolder } from "#app/utils/common"; +import { BerryType } from "#enums/berry-type"; +import { HeldItems } from "#enums/held-items"; + +interface BerryTypeToHeldItemMap { + [key: number]: HeldItems; +} + +export const berryTypeToHeldItem: BerryTypeToHeldItemMap = { + [BerryType.SITRUS]: HeldItems.SITRUS_BERRY, + [BerryType.LUM]: HeldItems.LUM_BERRY, + [BerryType.ENIGMA]: HeldItems.ENIGMA_BERRY, + [BerryType.LIECHI]: HeldItems.LIECHI_BERRY, + [BerryType.GANLON]: HeldItems.GANLON_BERRY, + [BerryType.PETAYA]: HeldItems.PETAYA_BERRY, + [BerryType.APICOT]: HeldItems.APICOT_BERRY, + [BerryType.SALAC]: HeldItems.SALAC_BERRY, + [BerryType.LANSAT]: HeldItems.LANSAT_BERRY, + [BerryType.STARF]: HeldItems.STARF_BERRY, + [BerryType.LEPPA]: HeldItems.LEPPA_BERRY, +}; + +export interface BERRY_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Whether the move was used by a player pokemon */ + isPlayer: boolean; +} + +// TODO: Maybe split up into subclasses? +export class BerryHeldItem extends ConsumableHeldItem { + public effects: ITEM_EFFECT[] = [ITEM_EFFECT.BERRY]; + public berryType: BerryType; + + constructor(berryType: BerryType, maxStackCount = 1) { + const type = berryTypeToHeldItem[berryType]; + super(type, maxStackCount); + + this.berryType = berryType; + } + + get name(): string { + return getBerryName(this.berryType); + } + + get description(): string { + return getBerryEffectDescription(this.berryType); + } + + get iconName(): string { + return `${BerryType[this.berryType].toLowerCase()}_berry`; + } + + /** + * Checks if {@linkcode BerryModifier} should be applied + * @param pokemon The {@linkcode Pokemon} that holds the berry + * @returns `true` if {@linkcode BerryModifier} should be applied + */ + // override shouldApply(pokemon: Pokemon): boolean { + // return !this.consumed && super.shouldApply(pokemon) && getBerryPredicate(this.berryType)(pokemon); + // } + + /** + * Applies {@linkcode BerryHeldItem} + * @param pokemon The {@linkcode Pokemon} that holds the berry + * @returns always `true` + */ + apply(params: BERRY_PARAMS): boolean { + const pokemon = params.pokemon; + const isPlayer = params.isPlayer; + + const preserve = new BooleanHolder(false); + globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); + const consumed = !preserve.value; + + // munch the berry and trigger unburden-like effects + getBerryEffectFunc(this.berryType)(pokemon); + this.consume(pokemon, isPlayer, consumed); + + // TODO: Update this method to work with held items + // Update berry eaten trackers for Belch, Harvest, Cud Chew, etc. + // Don't recover it if we proc berry pouch (no item duplication) + pokemon.recordEatenBerry(this.berryType, consumed); + + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + if ([BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(this.berryType)) { + return 2; + } + return 3; + } +} diff --git a/src/items/held-items/hit-heal.ts b/src/items/held-items/hit-heal.ts index dd2f8c73eeb..2117572e6cf 100644 --- a/src/items/held-items/hit-heal.ts +++ b/src/items/held-items/hit-heal.ts @@ -22,7 +22,7 @@ export class HitHealHeldItem extends HeldItem { return i18next.t("modifierType:ModifierType.SHELL_BELL.description"); } - get icon(): string { + get iconName(): string { return "shell_bell"; } diff --git a/src/items/held-items/reset-negative-stat-stage.ts b/src/items/held-items/reset-negative-stat-stage.ts index 90a2e2cd8d6..9e236a8f458 100644 --- a/src/items/held-items/reset-negative-stat-stage.ts +++ b/src/items/held-items/reset-negative-stat-stage.ts @@ -58,7 +58,7 @@ export class ResetNegativeStatStageHeldItem extends ConsumableHeldItem { }), ); - this.consume(pokemon, isPlayer); + this.consume(pokemon, isPlayer, true, false); } return statRestored; diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 2ee938d7f96..e707804512b 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -133,6 +133,8 @@ import { HeldItems } from "#enums/held-items"; import { allHeldItems } from "#app/items/all-held-items"; import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; import { attackTypeToHeldItem } from "#app/items/held-items/attack-type-booster"; +import { berryTypeToHeldItem } from "#app/items/held-items/berry"; +import { permanentStatToHeldItem, statBoostItems } from "#app/items/held-items/base-stat-booster"; const outputModifierData = false; const useMaxWeightForOutput = false; @@ -859,6 +861,26 @@ export class BerryModifierType extends PokemonHeldItemModifierType implements Ge } } +export class BerryReward extends PokemonHeldItemReward implements GeneratedPersistentModifierType { + private berryType: BerryType; + + constructor(berryType: BerryType) { + const itemId = berryTypeToHeldItem[berryType]; + super( + itemId, + // Next argument is useless + (type, args) => new BerryModifier(type, (args[0] as Pokemon).id, berryType), + ); + + this.berryType = berryType; + this.id = "BERRY"; // needed to prevent harvest item deletion; remove after modifier rework + } + + getPregenArgs(): any[] { + return [this.berryType]; + } +} + export class AttackTypeBoosterReward extends PokemonHeldItemReward implements GeneratedPersistentModifierType { public moveType: PokemonType; public boostPercent: number; @@ -1029,6 +1051,24 @@ export class BaseStatBoosterModifierType } } +export class BaseStatBoosterReward extends PokemonHeldItemReward implements GeneratedPersistentModifierType { + private stat: PermanentStat; + private key: string; + + constructor(stat: PermanentStat) { + const key = statBoostItems[stat]; + const itemId = permanentStatToHeldItem[stat]; + super(itemId, (_type, args) => new BaseStatModifier(this, (args[0] as Pokemon).id, this.stat)); + + this.stat = stat; + this.key = key; + } + + getPregenArgs(): any[] { + return [this.stat]; + } +} + /** * Shuckle Juice item */ @@ -1524,6 +1564,18 @@ class BaseStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { } } +class BaseStatBoosterRewardGenerator extends ModifierTypeGenerator { + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new BaseStatBoosterReward(pregenArgs[0]); + } + const randStat: PermanentStat = randSeedInt(Stat.SPD + 1); + return new BaseStatBoosterReward(randStat); + }); + } +} + class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator { public static readonly items: Record = { [Stat.ATK]: "x_attack", @@ -2143,6 +2195,8 @@ export const modifierTypes = { } })("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new TempCritBoosterModifier(type, 5)), + BASE_STAT_BOOSTER_REWARD: () => new BaseStatBoosterRewardGenerator(), + BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(), ATTACK_TYPE_BOOSTER_REWARD: () => new AttackTypeBoosterRewardGenerator(), @@ -2189,6 +2243,26 @@ export const modifierTypes = { return new TerastallizeModifierType(shardType); }), + BERRY_REWARD: () => + new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in BerryType) { + return new BerryModifierType(pregenArgs[0] as BerryType); + } + const berryTypes = getEnumValues(BerryType); + let randBerryType: BerryType; + const rand = randSeedInt(12); + if (rand < 2) { + randBerryType = BerryType.SITRUS; + } else if (rand < 4) { + randBerryType = BerryType.LUM; + } else if (rand < 6) { + randBerryType = BerryType.LEPPA; + } else { + randBerryType = berryTypes[randSeedInt(berryTypes.length - 3) + 2]; + } + return new BerryReward(randBerryType); + }), + BERRY: () => new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in BerryType) { @@ -3150,11 +3224,11 @@ const modifierPool: ModifierPool = { }; const wildModifierPool: ModifierPool = { - [ModifierTier.COMMON]: [new WeightedModifierType(modifierTypes.BERRY, 1)].map(m => { + [ModifierTier.COMMON]: [new WeightedModifierType(modifierTypes.BERRY_REWARD, 1)].map(m => { m.setTier(ModifierTier.COMMON); return m; }), - [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1)].map(m => { + [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER_REWARD, 1)].map(m => { m.setTier(ModifierTier.GREAT); return m; }), @@ -3177,19 +3251,19 @@ const wildModifierPool: ModifierPool = { const trainerModifierPool: ModifierPool = { [ModifierTier.COMMON]: [ - new WeightedModifierType(modifierTypes.BERRY, 8), - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), + new WeightedModifierType(modifierTypes.BERRY_REWARD, 8), + new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER_REWARD, 3), ].map(m => { m.setTier(ModifierTier.COMMON); return m; }), - [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3)].map(m => { + [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER_REWARD, 3)].map(m => { m.setTier(ModifierTier.GREAT); return m; }), [ModifierTier.ULTRA]: [ - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.WHITE_HERB, 0), + new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER_REWARD, 10), + new WeightedModifierType(modifierTypes.WHITE_HERB_REWARD, 0), ].map(m => { m.setTier(ModifierTier.ULTRA); return m; @@ -3698,22 +3772,27 @@ export function getEnemyModifierTypesForWave( return ret; } +// TODO: Add proper documentation to this function (once it fully works...) +// TODO: Convert trainer pool to HeldItems too export function getEnemyHeldItemsForWave( waveIndex: number, count: number, party: EnemyPokemon[], poolType: ModifierPoolType.WILD | ModifierPoolType.TRAINER, upgradeChance = 0, -): PokemonHeldItemReward[] { - const ret = new Array(count).fill(0).map( - () => - // TODO: Change this to get held items (this function really could just return a list of ids honestly) - getNewModifierTypeOption(party, poolType, undefined, upgradeChance && !randSeedInt(upgradeChance) ? 1 : 0) - ?.type as PokemonHeldItemReward, - ); +): HeldItems[] { + const ret = new Array(count).fill(0).map(() => { + const reward = getNewModifierTypeOption( + party, + poolType, + undefined, + upgradeChance && !randSeedInt(upgradeChance) ? 1 : 0, + )?.type as PokemonHeldItemReward; + return reward.itemId; + }); if (!(waveIndex % 1000)) { // TODO: Change this line with the actual held item when implemented - ret.push(getModifierType(modifierTypes.MINI_BLACK_HOLE) as PokemonHeldItemReward); + ret.push(HeldItems.MINI_BLACK_HOLE); } return ret; }