diff --git a/src/@types/trainer-funcs.ts b/src/@types/trainer-funcs.ts index 0546dd53024..d2a6127819c 100644 --- a/src/@types/trainer-funcs.ts +++ b/src/@types/trainer-funcs.ts @@ -1,5 +1,5 @@ import type { EnemyPokemon } from "#app/field/pokemon"; -import type { PersistentModifier } from "#app/modifier/modifier"; +import type { TrainerItemConfiguration } from "#app/items/trainer-item-data-types"; import type { PartyMemberStrength } from "#enums/party-member-strength"; import type { SpeciesId } from "#enums/species-id"; import type { TrainerConfig } from "../data/trainers/trainer-config"; @@ -7,7 +7,7 @@ import type { TrainerPartyTemplate } from "../data/trainers/TrainerPartyTemplate export type PartyTemplateFunc = () => TrainerPartyTemplate; export type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon; -export type GenModifiersFunc = (party: EnemyPokemon[]) => PersistentModifier[]; +export type GenTrainerItemsFunc = (party: EnemyPokemon[]) => TrainerItemConfiguration; export type GenAIFunc = (party: EnemyPokemon[]) => void; export interface TrainerTierPools { diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 721b55020f9..5fa6df23051 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -5,7 +5,7 @@ import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import type { PokemonSpeciesFilter } from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; -import { allSpecies } from "#app/data/data-lists"; +import { allHeldItems, allSpecies, allTrainerItems } from "#app/data/data-lists"; import { fixedInt, getIvsFromId, @@ -21,23 +21,12 @@ import { isBetween, } from "#app/utils/common"; import { deepMergeSpriteData } from "#app/utils/data"; -import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier"; +import type { Modifier } from "./modifier/modifier"; import { ConsumableModifier, ConsumablePokemonModifier, - DoubleBattleChanceBoosterModifier, - ExpBalanceModifier, - ExpShareModifier, FusePokemonModifier, - HealingBoosterModifier, - ModifierBar, - MultipleParticipantExpBonusModifier, - PersistentModifier, - PokemonExpBoosterModifier, - PokemonFormChangeItemModifier, - PokemonHeldItemModifier, PokemonHpRestoreModifier, - PokemonIncrementingStatModifier, RememberMoveModifier, } from "./modifier/modifier"; import { PokeballType } from "#enums/pokeball"; @@ -55,18 +44,9 @@ import { GameData } from "#app/system/game-data"; import { addTextObject, getTextColor, TextStyle } from "#app/ui/text"; import { allMoves } from "./data/data-lists"; import { MusicPreference } from "#app/system/settings/settings"; -import { - getDefaultModifierTypeForTier, - getEnemyModifierTypesForWave, - getLuckString, - getLuckTextTint, - getPartyLuckValue, - PokemonHeldItemModifierType, -} from "#app/modifier/modifier-type"; -import { getModifierType } from "./utils/modifier-utils"; -import { modifierTypes } from "./data/data-lists"; +import { getLuckString, getLuckTextTint, getPartyLuckValue } from "#app/modifier/modifier-type"; import { getModifierPoolForType } from "./utils/modifier-utils"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; +import { HeldItemPoolType, ModifierPoolType } from "#enums/modifier-pool-type"; import AbilityBar from "#app/ui/ability-bar"; import { applyAbAttrs } from "./data/abilities/apply-ab-attrs"; import { allAbilities } from "./data/data-lists"; @@ -89,7 +69,7 @@ import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import PokeballTray from "#app/ui/pokeball-tray"; import InvertPostFX from "#app/pipelines/invert"; import type { Achv } from "#app/system/achv"; -import { achvs, ModifierAchv, MoneyAchv } from "#app/system/achv"; +import { achvs, HeldItemAchv, ModifierAchv, MoneyAchv } from "#app/system/achv"; import type { Voucher } from "#app/system/voucher"; import { vouchers } from "#app/system/voucher"; import { Gender } from "#app/data/gender"; @@ -149,7 +129,6 @@ import { import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import type HeldModifierConfig from "#app/@types/held-modifier-config"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -161,7 +140,24 @@ import { hasExpSprite } from "./sprites/sprite-utils"; import { timedEventManager } from "./global-event-manager"; import { starterColors } from "./global-vars/starter-colors"; import { startingWave } from "./starting-wave"; +import { applyHeldItems } from "./items/all-held-items"; +import { HELD_ITEM_EFFECT } from "./items/held-item"; import { PhaseManager } from "./phase-manager"; +import { HeldItemId } from "#enums/held-item-id"; +import { assignEnemyHeldItemsForWave, assignItemsFromConfiguration } from "./items/held-item-pool"; +import type { HeldItemConfiguration } from "./items/held-item-data-types"; +import { TrainerItemManager } from "./items/trainer-item-manager"; +import { type EnemyAttackStatusEffectChanceTrainerItem, TRAINER_ITEM_EFFECT } from "./items/trainer-item"; +import { applyTrainerItems, type APPLY_TRAINER_ITEMS_PARAMS } from "./items/apply-trainer-items"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { + isTrainerItemPool, + isTrainerItemSpecs, + type TrainerItemSaveData, + type TrainerItemConfiguration, +} from "./items/trainer-item-data-types"; +import { getNewTrainerItemFromPool } from "./items/trainer-item-pool"; +import { ItemBar } from "./ui/item-bar-ui"; const DEBUG_RNG = false; @@ -307,8 +303,8 @@ export default class BattleScene extends SceneBase { private scoreText: Phaser.GameObjects.Text; private luckLabelText: Phaser.GameObjects.Text; private luckText: Phaser.GameObjects.Text; - private modifierBar: ModifierBar; - private enemyModifierBar: ModifierBar; + private itemBar: ItemBar; + private enemyItemBar: ItemBar; public arenaFlyout: ArenaFlyout; private fieldOverlay: Phaser.GameObjects.Rectangle; @@ -316,8 +312,8 @@ export default class BattleScene extends SceneBase { private shopOverlayShown = false; private shopOverlayOpacity = 0.8; - public modifiers: PersistentModifier[]; - private enemyModifiers: PersistentModifier[]; + public trainerItems: TrainerItemManager; + public enemyTrainerItems: TrainerItemManager; public uiContainer: Phaser.GameObjects.Container; public ui: UI; @@ -498,18 +494,18 @@ export default class BattleScene extends SceneBase { this.shopOverlay.setAlpha(0); this.fieldUI.add(this.shopOverlay); - this.modifiers = []; - this.enemyModifiers = []; + this.trainerItems = new TrainerItemManager(); + this.enemyTrainerItems = new TrainerItemManager(); - this.modifierBar = new ModifierBar(); - this.modifierBar.setName("modifier-bar"); - this.add.existing(this.modifierBar); - uiContainer.add(this.modifierBar); + this.itemBar = new ItemBar(); + this.itemBar.setName("modifier-bar"); + this.add.existing(this.itemBar); + uiContainer.add(this.itemBar); - this.enemyModifierBar = new ModifierBar(true); - this.enemyModifierBar.setName("enemy-modifier-bar"); - this.add.existing(this.enemyModifierBar); - uiContainer.add(this.enemyModifierBar); + this.enemyItemBar = new ItemBar(true); + this.enemyItemBar.setName("enemy-modifier-bar"); + this.add.existing(this.enemyItemBar); + uiContainer.add(this.enemyItemBar); this.charSprite = new CharSprite(); this.charSprite.setName("sprite-char"); @@ -879,8 +875,8 @@ export default class BattleScene extends SceneBase { * @param isEnemy Whether to return the enemy's modifier bar * @returns {ModifierBar} */ - getModifierBar(isEnemy?: boolean): ModifierBar { - return isEnemy ? this.enemyModifierBar : this.modifierBar; + getItemBar(isEnemy?: boolean): ItemBar { + return isEnemy ? this.enemyItemBar : this.itemBar; } // store info toggles to be accessible by the ui @@ -918,6 +914,7 @@ export default class BattleScene extends SceneBase { variant?: Variant, ivs?: number[], nature?: Nature, + heldItemConfig?: HeldItemConfiguration, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void, ): PlayerPokemon { @@ -931,6 +928,7 @@ export default class BattleScene extends SceneBase { variant, ivs, nature, + heldItemConfig, dataSource, ); @@ -969,6 +967,7 @@ export default class BattleScene extends SceneBase { trainerSlot: TrainerSlot, boss = false, shinyLock = false, + heldItemConfig?: HeldItemConfiguration, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void, ): EnemyPokemon { @@ -981,7 +980,7 @@ export default class BattleScene extends SceneBase { boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1; } - const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, dataSource); + const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, heldItemConfig, dataSource); if (Overrides.OPP_FUSION_OVERRIDE) { pokemon.generateFusionSpecies(); } @@ -1025,6 +1024,7 @@ export default class BattleScene extends SceneBase { } pokemon.init(); + return pokemon; } @@ -1045,7 +1045,7 @@ export default class BattleScene extends SceneBase { this.field.remove(pokemon, true); pokemon.destroy(); } - this.updateModifiers(true); + this.updateItems(true); } addPokemonIcon( @@ -1196,10 +1196,10 @@ export default class BattleScene extends SceneBase { this.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; } - this.modifiers = []; - this.enemyModifiers = []; - this.modifierBar.removeAll(true); - this.enemyModifierBar.removeAll(true); + this.trainerItems.clearItems(); + this.enemyTrainerItems.clearItems(); + this.itemBar.removeAll(true); + this.enemyItemBar.removeAll(true); for (const p of this.getPlayerParty()) { p.destroy(); @@ -1301,7 +1301,7 @@ export default class BattleScene extends SceneBase { getDoubleBattleChance(newWaveIndex: number, playerField: PlayerPokemon[]) { const doubleChance = new NumberHolder(newWaveIndex % 10 === 0 ? 32 : 8); - this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance); + this.applyPlayerItems(TRAINER_ITEM_EFFECT.DOUBLE_BATTLE_CHANCE_BOOSTER, { numberHolder: doubleChance }); for (const p of playerField) { applyAbAttrs("DoubleBattleChanceAbAttr", { pokemon: p, chance: doubleChance }); } @@ -2003,11 +2003,11 @@ export default class BattleScene extends SceneBase { } showEnemyModifierBar(): void { - this.enemyModifierBar.setVisible(true); + this.enemyItemBar.setVisible(true); } hideEnemyModifierBar(): void { - this.enemyModifierBar.setVisible(false); + this.enemyItemBar.setVisible(false); } updateBiomeWaveText(): void { @@ -2100,7 +2100,7 @@ export default class BattleScene extends SceneBase { } updateUIPositions(): void { - const enemyModifierCount = this.enemyModifiers.filter(m => m.isIconVisible()).length; + const enemyModifierCount = this.enemyItemBar.totalVisibleLength; const biomeWaveTextHeight = this.biomeWaveText.getBottomLeft().y - this.biomeWaveText.getTopLeft().y; this.biomeWaveText.setY( -(this.game.canvas.height / 6) + @@ -2132,9 +2132,7 @@ export default class BattleScene extends SceneBase { enemy.getSpeciesForm().getBaseExp() * (enemy.level / this.getMaxExpLevel()) * ((enemy.ivs.reduce((iv: number, total: number) => (total += iv), 0) / 93) * 0.2 + 0.8); - this.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemy.id, false).map( - m => (scoreIncrease *= (m as PokemonHeldItemModifier).getScoreMultiplier()), - ); + enemy.getHeldItems().map(m => (scoreIncrease *= allHeldItems[m].getScoreMultiplier())); if (enemy.isBoss()) { scoreIncrease *= Math.sqrt(enemy.bossSegments); } @@ -2652,14 +2650,11 @@ export default class BattleScene extends SceneBase { return Math.floor(moneyValue / 10) * 10; } - addModifier( - modifier: Modifier | null, - ignoreUpdate?: boolean, - playSound?: boolean, - virtual?: boolean, - instant?: boolean, - cost?: number, - ): boolean { + applyPlayerItems(effect: T, params: APPLY_TRAINER_ITEMS_PARAMS[T]) { + applyTrainerItems(effect, this.trainerItems, params); + } + + addModifier(modifier: Modifier | null, playSound?: boolean, instant?: boolean, cost?: number): boolean { // We check against modifier.type to stop a bug related to loading in a pokemon that has a form change item, which prior to some patch // that changed form change modifiers worked, had previously set the `type` field to null. // TODO: This is not the right place to check for this; it should ideally go in a session migrator. @@ -2669,40 +2664,7 @@ export default class BattleScene extends SceneBase { let success = false; const soundName = modifier.type.soundName; this.validateAchvs(ModifierAchv, modifier); - const modifiersToRemove: PersistentModifier[] = []; - if (modifier instanceof PersistentModifier) { - if ((modifier as PersistentModifier).add(this.modifiers, !!virtual)) { - if (modifier instanceof PokemonFormChangeItemModifier) { - const pokemon = this.getPokemonById(modifier.pokemonId); - if (pokemon) { - success = modifier.apply(pokemon, true); - } - } - if (playSound && !this.sound.get(soundName)) { - this.playSound(soundName); - } - } else if (!virtual) { - const defaultModifierType = getDefaultModifierTypeForTier(modifier.type.tier); - this.phaseManager.queueMessage( - i18next.t("battle:itemStackFull", { - fullItemName: modifier.type.name, - itemName: defaultModifierType.name, - }), - undefined, - false, - 3000, - ); - return this.addModifier(defaultModifierType.newModifier(), ignoreUpdate, playSound, false, instant); - } - - for (const rm of modifiersToRemove) { - this.removeModifier(rm); - } - - if (!ignoreUpdate && !virtual) { - this.updateModifiers(true, instant); - } - } else if (modifier instanceof ConsumableModifier) { + if (modifier instanceof ConsumableModifier) { if (playSound && !this.sound.get(soundName)) { this.playSound(soundName); } @@ -2715,7 +2677,7 @@ export default class BattleScene extends SceneBase { if (modifier instanceof PokemonHpRestoreModifier) { if (!(modifier as PokemonHpRestoreModifier).fainted) { const hpRestoreMultiplier = new NumberHolder(1); - this.applyModifiers(HealingBoosterModifier, true, hpRestoreMultiplier); + this.applyPlayerItems(TRAINER_ITEM_EFFECT.HEALING_BOOSTER, { numberHolder: hpRestoreMultiplier }); args.push(hpRestoreMultiplier.value); } else { args.push(1); @@ -2744,35 +2706,28 @@ export default class BattleScene extends SceneBase { return success; } - addEnemyModifier(modifier: PersistentModifier, ignoreUpdate?: boolean, instant?: boolean): Promise { - return new Promise(resolve => { - const modifiersToRemove: PersistentModifier[] = []; - if ((modifier as PersistentModifier).add(this.enemyModifiers, false)) { - if (modifier instanceof PokemonFormChangeItemModifier) { - const pokemon = this.getPokemonById(modifier.pokemonId); - if (pokemon) { - modifier.apply(pokemon, true); - } - } - for (const rm of modifiersToRemove) { - this.removeModifier(rm, true); - } - } - if (!ignoreUpdate) { - this.updateModifiers(false, instant); - resolve(); - } else { - resolve(); - } - }); + addHeldItem(heldItemId: HeldItemId, pokemon: Pokemon, amount = 1, playSound?: boolean, ignoreUpdate?: boolean) { + pokemon.heldItemManager.add(heldItemId, amount); + if (!ignoreUpdate) { + this.updateItems(pokemon.isPlayer()); + } + const soundName = allHeldItems[heldItemId].soundName; + if (playSound && !this.sound.get(soundName)) { + this.playSound(soundName); + } + + if (pokemon.isPlayer()) { + this.validateAchvs(HeldItemAchv, pokemon); + } } /** - * Try to transfer a held item to another pokemon. + * Try to transfer a held item from source to target. * If the recepient already has the maximum amount allowed for this item, the transfer is cancelled. * The quantity to transfer is automatically capped at how much the recepient can take before reaching the maximum stack size for the item. * A transfer that moves a quantity smaller than what is specified in the transferQuantity parameter is still considered successful. - * @param itemModifier {@linkcode PokemonHeldItemModifier} item to transfer (represents the whole stack) + * @param heldItemId {@linkcode HeldItemId} item to transfer + * @param source {@linkcode Pokemon} giver in this transfer * @param target {@linkcode Pokemon} recepient in this transfer * @param playSound `true` to play a sound when transferring the item * @param transferQuantity How many items of the stack to transfer. Optional, defaults to `1` @@ -2781,16 +2736,15 @@ export default class BattleScene extends SceneBase { * @param itemLost If `true`, treat the item's current holder as losing the item (for now, this simply enables Unburden). Default is `true`. * @returns `true` if the transfer was successful */ - tryTransferHeldItemModifier( - itemModifier: PokemonHeldItemModifier, + tryTransferHeldItem( + heldItemId: HeldItemId, + source: Pokemon, target: Pokemon, playSound: boolean, transferQuantity = 1, - instant?: boolean, ignoreUpdate?: boolean, itemLost = true, ): boolean { - const source = itemModifier.pokemonId ? itemModifier.getPokemon() : null; const cancelled = new BooleanHolder(false); if (source && source.isPlayer() !== target.isPlayer()) { @@ -2801,112 +2755,94 @@ export default class BattleScene extends SceneBase { return false; } - const newItemModifier = itemModifier.clone() as PokemonHeldItemModifier; - newItemModifier.pokemonId = target.id; - const matchingModifier = this.findModifier( - m => m instanceof PokemonHeldItemModifier && m.matchType(itemModifier) && m.pokemonId === target.id, - target.isPlayer(), - ) as PokemonHeldItemModifier; + const itemStack = source.heldItemManager.getStack(heldItemId); + const matchingItemStack = target.heldItemManager.getStack(heldItemId); - if (matchingModifier) { - const maxStackCount = matchingModifier.getMaxStackCount(); - if (matchingModifier.stackCount >= maxStackCount) { - return false; - } - const countTaken = Math.min( - transferQuantity, - itemModifier.stackCount, - maxStackCount - matchingModifier.stackCount, - ); - itemModifier.stackCount -= countTaken; - newItemModifier.stackCount = matchingModifier.stackCount + countTaken; - } else { - const countTaken = Math.min(transferQuantity, itemModifier.stackCount); - itemModifier.stackCount -= countTaken; - newItemModifier.stackCount = countTaken; + const maxStackCount = allHeldItems[heldItemId].getMaxStackCount(); + if (matchingItemStack >= maxStackCount) { + return false; + } + const countTaken = Math.min(transferQuantity, itemStack, maxStackCount - matchingItemStack); + + const itemSpecs = source.heldItemManager.getItemSpecs(heldItemId); + if (!itemSpecs) { + return false; + } + source.heldItemManager.remove(heldItemId, countTaken); + target.heldItemManager.add(itemSpecs); + + if (source.heldItemManager.getStack(heldItemId) === 0 && itemLost) { + applyAbAttrs("PostItemLostAbAttr", { pokemon: source }); } - const removeOld = itemModifier.stackCount === 0; + if (source.isPlayer() !== target.isPlayer() && !ignoreUpdate) { + this.updateItems(source.isPlayer()); + } - if (!removeOld || !source || this.removeModifier(itemModifier, source.isEnemy())) { - const addModifier = () => { - if (!matchingModifier || this.removeModifier(matchingModifier, target.isEnemy())) { - if (target.isPlayer()) { - this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant); - if (source && itemLost) { - applyAbAttrs("PostItemLostAbAttr", { pokemon: source }); - } - return true; + const soundName = allHeldItems[heldItemId].soundName; + if (playSound && !this.sound.get(soundName)) { + this.playSound(soundName); + } + + return true; + } + + canTransferHeldItem(heldItemId: HeldItemId, source: Pokemon, target: Pokemon, transferQuantity = 1): boolean { + const cancelled = new BooleanHolder(false); + + if (source && source.isPlayer() !== target.isPlayer()) { + applyAbAttrs("BlockItemTheftAbAttr", { pokemon: source, cancelled }); + } + + if (cancelled.value) { + return false; + } + + const itemStack = source.heldItemManager.getStack(heldItemId); + const matchingItemStack = target.heldItemManager.getStack(heldItemId); + + const maxStackCount = allHeldItems[heldItemId].getMaxStackCount(); + if (matchingItemStack >= maxStackCount) { + return false; + } + const countTaken = Math.min(transferQuantity, itemStack, maxStackCount - matchingItemStack); + + return countTaken > 0; + } + + assignTrainerItemsFromConfiguration(config: TrainerItemConfiguration, isPlayer: boolean) { + const manager = isPlayer ? this.trainerItems : this.enemyTrainerItems; + config.forEach(item => { + const { entry, count } = item; + const actualCount = typeof count === "function" ? count() : count; + + if (typeof entry === "number") { + manager.add(entry, actualCount); + } + + if (isTrainerItemSpecs(entry)) { + manager.add(entry); + } + + if (isTrainerItemPool(entry)) { + for (let i = 1; i <= (actualCount ?? 1); i++) { + const newItem = getNewTrainerItemFromPool(entry, manager); + if (newItem) { + manager.add(newItem); } - this.addEnemyModifier(newItemModifier, ignoreUpdate, instant); - if (source && itemLost) { - applyAbAttrs("PostItemLostAbAttr", { pokemon: source }); - } - return true; } - return false; - }; - if (source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) { - this.updateModifiers(source.isPlayer(), instant); - addModifier(); - } else { - addModifier(); } - return true; - } - return false; - } - - canTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, transferQuantity = 1): boolean { - const mod = itemModifier.clone() as PokemonHeldItemModifier; - const source = mod.pokemonId ? mod.getPokemon() : null; - const cancelled = new BooleanHolder(false); - - if (source && source.isPlayer() !== target.isPlayer()) { - applyAbAttrs("BlockItemTheftAbAttr", { pokemon: source, cancelled }); - } - - if (cancelled.value) { - return false; - } - - const matchingModifier = this.findModifier( - m => m instanceof PokemonHeldItemModifier && m.matchType(mod) && m.pokemonId === target.id, - target.isPlayer(), - ) as PokemonHeldItemModifier; - - if (matchingModifier) { - const maxStackCount = matchingModifier.getMaxStackCount(); - if (matchingModifier.stackCount >= maxStackCount) { - return false; - } - const countTaken = Math.min(transferQuantity, mod.stackCount, maxStackCount - matchingModifier.stackCount); - mod.stackCount -= countTaken; - } else { - const countTaken = Math.min(transferQuantity, mod.stackCount); - mod.stackCount -= countTaken; - } - - const removeOld = mod.stackCount === 0; - - return !removeOld || !source || this.hasModifier(itemModifier, !source.isPlayer()); - } - - removePartyMemberModifiers(partyMemberIndex: number): Promise { - return new Promise(resolve => { - const pokemonId = this.getPlayerParty()[partyMemberIndex].id; - const modifiersToRemove = this.modifiers.filter( - m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId, - ); - for (const m of modifiersToRemove) { - this.modifiers.splice(this.modifiers.indexOf(m), 1); - } - this.updateModifiers(); - resolve(); }); } - generateEnemyModifiers(heldModifiersConfigs?: HeldModifierConfig[][]): Promise { + assignTrainerItemsFromSaveData(saveData: TrainerItemSaveData, isPlayer: boolean) { + const manager = isPlayer ? this.trainerItems : this.enemyTrainerItems; + for (const item of saveData) { + manager.add(item); + } + } + + generateEnemyItems(heldItemConfigs?: HeldItemConfiguration[]): Promise { return new Promise(resolve => { if (this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { return resolve(); @@ -2921,26 +2857,13 @@ export default class BattleScene extends SceneBase { const party = this.getEnemyParty(); if (this.currentBattle.trainer) { - const modifiers = this.currentBattle.trainer.genModifiers(party); - for (const modifier of modifiers) { - this.addEnemyModifier(modifier, true, true); - } + const trainerItemConfig = this.currentBattle.trainer.genTrainerItems(party); + this.assignTrainerItemsFromConfiguration(trainerItemConfig, false); } party.forEach((enemyPokemon: EnemyPokemon, i: number) => { - if (heldModifiersConfigs && i < heldModifiersConfigs.length && heldModifiersConfigs[i]) { - for (const mt of heldModifiersConfigs[i]) { - let modifier: PokemonHeldItemModifier; - if (mt.modifier instanceof PokemonHeldItemModifierType) { - modifier = mt.modifier.newModifier(enemyPokemon); - } else { - modifier = mt.modifier as PokemonHeldItemModifier; - modifier.pokemonId = enemyPokemon.id; - } - modifier.stackCount = mt.stackCount ?? 1; - modifier.isTransferable = mt.isTransferable ?? modifier.isTransferable; - this.addEnemyModifier(modifier, true); - } + if (heldItemConfigs && i < heldItemConfigs.length && heldItemConfigs[i]) { + assignItemsFromConfiguration(heldItemConfigs[i], enemyPokemon); } else { const isBoss = enemyPokemon.isBoss() || @@ -2961,17 +2884,17 @@ export default class BattleScene extends SceneBase { if (isBoss) { count = Math.max(count, Math.floor(chances / 2)); } - getEnemyModifierTypesForWave( + assignEnemyHeldItemsForWave( difficultyWaveIndex, count, - [enemyPokemon], - this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, + enemyPokemon, + this.currentBattle.battleType === BattleType.TRAINER ? HeldItemPoolType.TRAINER : HeldItemPoolType.WILD, upgradeChance, - ).map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false)); + ); } return true; }); - this.updateModifiers(false); + this.updateItems(false); resolve(); }); } @@ -2979,74 +2902,41 @@ export default class BattleScene extends SceneBase { /** * Removes all modifiers from enemy pokemon of {@linkcode PersistentModifier} type */ - clearEnemyModifiers(): void { - const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PersistentModifier); - for (const m of modifiersToRemove) { - this.enemyModifiers.splice(this.enemyModifiers.indexOf(m), 1); + clearEnemyItems(): void { + this.enemyTrainerItems.clearItems(); + for (const p of this.getEnemyParty()) { + p.heldItemManager.clearItems(); } - this.updateModifiers(false); - this.updateUIPositions(); - } - - /** - * Removes all modifiers from enemy pokemon of {@linkcode PokemonHeldItemModifier} type - * @param pokemon - If specified, only removes held items from that {@linkcode Pokemon} - */ - clearEnemyHeldItemModifiers(pokemon?: Pokemon): void { - const modifiersToRemove = this.enemyModifiers.filter( - m => m instanceof PokemonHeldItemModifier && (!pokemon || m.getPokemon() === pokemon), - ); - for (const m of modifiersToRemove) { - this.enemyModifiers.splice(this.enemyModifiers.indexOf(m), 1); - } - this.updateModifiers(false); + this.updateItems(false); this.updateUIPositions(); } setModifiersVisible(visible: boolean) { - [this.modifierBar, this.enemyModifierBar].map(m => m.setVisible(visible)); + [this.itemBar, this.enemyItemBar].map(m => m.setVisible(visible)); } // TODO: Document this - updateModifiers(player = true, instant?: boolean): void { - const modifiers = player ? this.modifiers : (this.enemyModifiers as PersistentModifier[]); - for (let m = 0; m < modifiers.length; m++) { - const modifier = modifiers[m]; - if ( - modifier instanceof PokemonHeldItemModifier && - !this.getPokemonById((modifier as PokemonHeldItemModifier).pokemonId) - ) { - modifiers.splice(m--, 1); - } - if ( - modifier instanceof PokemonHeldItemModifier && - !isNullOrUndefined(modifier.getSpecies()) && - !this.getPokemonById(modifier.pokemonId)?.hasSpecies(modifier.getSpecies()!) - ) { - modifiers.splice(m--, 1); - } - } - for (const modifier of modifiers) { - if (modifier instanceof PersistentModifier) { - (modifier as PersistentModifier).virtualStackCount = 0; - } + updateItems(player = true, showHeldItems = true): void { + const trainerItems = player ? this.trainerItems : this.enemyTrainerItems; + + this.updateParty(player ? this.getPlayerParty() : this.getEnemyParty(), true); + + const pokemonA = player ? this.getPlayerParty()[0] : this.getEnemyParty()[0]; + + const bar = player ? this.itemBar : this.enemyItemBar; + + if (showHeldItems) { + bar.updateItems(trainerItems, pokemonA); + } else { + bar.updateItems(trainerItems); } - const modifiersClone = modifiers.slice(0); - for (const modifier of modifiersClone) { - if (!modifier.getStackCount()) { - modifiers.splice(modifiers.indexOf(modifier), 1); - } - } - - this.updatePartyForModifiers(player ? this.getPlayerParty() : this.getEnemyParty(), instant); - (player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers); if (!player) { this.updateUIPositions(); } } - updatePartyForModifiers(party: Pokemon[], instant?: boolean): Promise { + updateParty(party: Pokemon[], instant?: boolean): Promise { return new Promise(resolve => { Promise.allSettled( party.map(p => { @@ -3057,156 +2947,33 @@ export default class BattleScene extends SceneBase { }); } - hasModifier(modifier: PersistentModifier, enemy = false): boolean { - const modifiers = !enemy ? this.modifiers : this.enemyModifiers; - return modifiers.indexOf(modifier) > -1; - } + applyShuffledStatusTokens(pokemon: Pokemon) { + let tokens = [ + TrainerItemId.ENEMY_ATTACK_BURN_CHANCE, + TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE, + TrainerItemId.ENEMY_ATTACK_POISON_CHANCE, + ].filter(t => this.enemyTrainerItems.hasItem(t)); - /** - * Removes a currently owned item. If the item is stacked, the entire item stack - * gets removed. This function does NOT apply in-battle effects, such as Unburden. - * If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead. - * @param modifier The item to be removed. - * @param enemy `true` to remove an item owned by the enemy rather than the player; default `false`. - * @returns `true` if the item exists and was successfully removed, `false` otherwise - */ - removeModifier(modifier: PersistentModifier, enemy = false): boolean { - const modifiers = !enemy ? this.modifiers : this.enemyModifiers; - const modifierIndex = modifiers.indexOf(modifier); - if (modifierIndex > -1) { - modifiers.splice(modifierIndex, 1); - if (modifier instanceof PokemonFormChangeItemModifier) { - const pokemon = this.getPokemonById(modifier.pokemonId); - if (pokemon) { - modifier.apply(pokemon, false); - } - } - return true; - } - - return false; - } - - /** - * Get all of the modifiers that match `modifierType` - * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @returns the list of all modifiers that matched `modifierType`. - */ - getModifiers(modifierType: Constructor, player = true): T[] { - return (player ? this.modifiers : this.enemyModifiers).filter((m): m is T => m instanceof modifierType); - } - - /** - * Get all of the modifiers that pass the `modifierFilter` function - * @param modifierFilter The function used to filter a target's modifiers - * @param isPlayer Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @returns the list of all modifiers that passed the `modifierFilter` function - */ - findModifiers(modifierFilter: ModifierPredicate, isPlayer = true): PersistentModifier[] { - return (isPlayer ? this.modifiers : this.enemyModifiers).filter(modifierFilter); - } - - /** - * Find the first modifier that pass the `modifierFilter` function - * @param modifierFilter The function used to filter a target's modifiers - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @returns the first modifier that passed the `modifierFilter` function; `undefined` if none passed - */ - findModifier(modifierFilter: ModifierPredicate, player = true): PersistentModifier | undefined { - return (player ? this.modifiers : this.enemyModifiers).find(modifierFilter); - } - - /** - * Apply all modifiers that match `modifierType` in a random order - * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` - * @returns the list of all modifiers that matched `modifierType` and were applied. - */ - applyShuffledModifiers( - modifierType: Constructor, - player = true, - ...args: Parameters - ): T[] { - let modifiers = (player ? this.modifiers : this.enemyModifiers).filter( - (m): m is T => m instanceof modifierType && m.shouldApply(...args), - ); this.executeWithSeedOffset( () => { - const shuffleModifiers = mods => { - if (mods.length < 1) { - return mods; + const shuffleTokens = toks => { + if (toks.length < 1) { + return toks; } - const rand = randSeedInt(mods.length); - return [mods[rand], ...shuffleModifiers(mods.filter((_, i) => i !== rand))]; + const rand = randSeedInt(toks.length); + return [toks[rand], ...shuffleTokens(toks.filter((_, i) => i !== rand))]; }; - modifiers = shuffleModifiers(modifiers); + tokens = shuffleTokens(tokens); }, this.currentBattle.turn << 4, this.waveSeed, ); - return this.applyModifiersInternal(modifiers, player, args); - } - /** - * Apply all modifiers that match `modifierType` - * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` - * @returns the list of all modifiers that matched `modifierType` and were applied. - */ - applyModifiers( - modifierType: Constructor, - player = true, - ...args: Parameters - ): T[] { - const modifiers = (player ? this.modifiers : this.enemyModifiers).filter( - (m): m is T => m instanceof modifierType && m.shouldApply(...args), - ); - return this.applyModifiersInternal(modifiers, player, args); - } - - /** Helper function to apply all passed modifiers */ - applyModifiersInternal( - modifiers: T[], - player: boolean, - args: Parameters, - ): T[] { - const appliedModifiers: T[] = []; - for (const modifier of modifiers) { - if (modifier.apply(...args)) { - console.log("Applied", modifier.type.name, !player ? "(enemy)" : ""); - appliedModifiers.push(modifier); - } + for (const t in tokens) { + (allTrainerItems[t] as EnemyAttackStatusEffectChanceTrainerItem).apply(this.enemyTrainerItems, { + pokemon: pokemon, + }); } - - return appliedModifiers; - } - - /** - * Apply the first modifier that matches `modifierType` - * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` - * @returns the first modifier that matches `modifierType` and was applied; return `null` if none matched - */ - applyModifier( - modifierType: Constructor, - player = true, - ...args: Parameters - ): T | null { - const modifiers = (player ? this.modifiers : this.enemyModifiers).filter( - (m): m is T => m instanceof modifierType && m.shouldApply(...args), - ); - for (const modifier of modifiers) { - if (modifier.apply(...args)) { - console.log("Applied", modifier.type.name, !player ? "(enemy)" : ""); - return modifier; - } - } - - return null; } triggerPokemonFormChange( @@ -3223,22 +2990,17 @@ export default class BattleScene extends SceneBase { let matchingFormChange: SpeciesFormChange | null; if (pokemon.species.speciesId === SpeciesId.NECROZMA && matchingFormChangeOpts.length > 1) { // Ultra Necrozma is changing its form back, so we need to figure out into which form it devolves. - const formChangeItemModifiers = ( - this.findModifiers( - m => m instanceof PokemonFormChangeItemModifier && m.pokemonId === pokemon.id, - ) as PokemonFormChangeItemModifier[] - ) - .filter(m => m.active) - .map(m => m.formChangeItem); + const activeFormChangeItems = pokemon.heldItemManager.getActiveFormChangeItems(); - matchingFormChange = formChangeItemModifiers.includes(FormChangeItem.N_LUNARIZER) + matchingFormChange = activeFormChangeItems.includes(FormChangeItem.N_LUNARIZER) ? matchingFormChangeOpts[0] - : formChangeItemModifiers.includes(FormChangeItem.N_SOLARIZER) + : activeFormChangeItems.includes(FormChangeItem.N_SOLARIZER) ? matchingFormChangeOpts[1] : null; } else { matchingFormChange = matchingFormChangeOpts[0]; } + if (matchingFormChange) { let phase: Phase; if (pokemon.isPlayer() && !matchingFormChange.quiet) { @@ -3368,11 +3130,7 @@ export default class BattleScene extends SceneBase { pokemon.species.name, undefined, () => { - const finalBossMBH = getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier( - pokemon, - ) as TurnHeldItemTransferModifier; - finalBossMBH.setTransferrableFalse(); - this.addEnemyModifier(finalBossMBH, false, true); + pokemon.heldItemManager.add(HeldItemId.MINI_BLACK_HOLE); pokemon.generateAndPopulateMoveset(1); this.setFieldScale(0.75); this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); @@ -3409,11 +3167,9 @@ export default class BattleScene extends SceneBase { ): void { const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds; const party = this.getPlayerParty(); - const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; - const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; - const multipleParticipantExpBonusModifier = this.findModifier( - m => m instanceof MultipleParticipantExpBonusModifier, - ) as MultipleParticipantExpBonusModifier; + const expShareStack = this.trainerItems.getStack(TrainerItemId.EXP_SHARE); + const expBalanceStack = this.trainerItems.getStack(TrainerItemId.EXP_BALANCE); + const ovalCharmStack = this.trainerItems.getStack(TrainerItemId.OVAL_CHARM); const nonFaintedPartyMembers = party.filter(p => p.hp); const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.getMaxExpLevel()); const partyMemberExp: number[] = []; @@ -3436,28 +3192,28 @@ export default class BattleScene extends SceneBase { const participated = participantIds.has(pId); if (participated && pokemonDefeated) { partyMember.addFriendship(FRIENDSHIP_GAIN_FROM_BATTLE); - const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier); - if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount()) { - machoBraceModifier.stackCount++; - this.updateModifiers(true, true); + const hasMachoBrace = partyMember.heldItemManager.hasItem(HeldItemId.MACHO_BRACE); + if (hasMachoBrace) { + partyMember.heldItemManager.add(HeldItemId.MACHO_BRACE); + this.updateItems(true); partyMember.updateInfo(); } } if (!expPartyMembers.includes(partyMember)) { continue; } - if (!participated && !expShareModifier) { + if (!participated && !expShareStack) { partyMemberExp.push(0); continue; } let expMultiplier = 0; if (participated) { expMultiplier += 1 / participantIds.size; - if (participantIds.size > 1 && multipleParticipantExpBonusModifier) { - expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; + if (participantIds.size > 1 && ovalCharmStack) { + expMultiplier += ovalCharmStack * 0.2; } - } else if (expShareModifier) { - expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size; + } else if (expShareStack) { + expMultiplier += (expShareStack * 0.2) / participantIds.size; } if (partyMember.pokerus) { expMultiplier *= 1.5; @@ -3466,11 +3222,11 @@ export default class BattleScene extends SceneBase { expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; } const pokemonExp = new NumberHolder(expValue * expMultiplier); - this.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); + applyHeldItems(HELD_ITEM_EFFECT.EXP_BOOSTER, { pokemon: partyMember, expAmount: pokemonExp }); partyMemberExp.push(Math.floor(pokemonExp.value)); } - if (expBalanceModifier) { + if (expBalanceStack) { let totalLevel = 0; let totalExp = 0; expPartyMembers.forEach((expPartyMember, epm) => { @@ -3493,7 +3249,7 @@ export default class BattleScene extends SceneBase { partyMemberExp[pm] = Phaser.Math.Linear( partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, - 0.2 * expBalanceModifier.getStackCount(), + 0.2 * expBalanceStack, ); }); } diff --git a/src/battle.ts b/src/battle.ts index 49f5c39e7dd..7242c4b8650 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -13,7 +13,6 @@ import { import Trainer from "./field/trainer"; import { TrainerVariant } from "#enums/trainer-variant"; import type { GameMode } from "./game-mode"; -import { MoneyMultiplierModifier, type PokemonHeldItemModifier } from "./modifier/modifier"; import type { PokeballType } from "#enums/pokeball"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { SpeciesFormKey } from "#enums/species-form-key"; @@ -31,11 +30,13 @@ import i18next from "#app/plugins/i18n"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import type { CustomModifierSettings } from "#app/modifier/modifier-type"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { BattleType } from "#enums/battle-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; +import type { HeldItemId } from "#enums/held-item-id"; import { BattlerIndex } from "#enums/battler-index"; +import { TRAINER_ITEM_EFFECT } from "./items/trainer-item"; export interface TurnCommand { command: Command; @@ -72,7 +73,7 @@ export default class Battle { public turnCommands: TurnCommands; public playerParticipantIds: Set = new Set(); public battleScore = 0; - public postBattleLoot: PokemonHeldItemModifier[] = []; + public postBattleLoot: HeldItemId[] = []; public escapeAttempts = 0; public lastMove: MoveId; public battleSeed: string = randomString(16, true); @@ -177,24 +178,12 @@ export default class Battle { } addPostBattleLoot(enemyPokemon: EnemyPokemon): void { - this.postBattleLoot.push( - ...globalScene - .findModifiers( - m => m.is("PokemonHeldItemModifier") && m.pokemonId === enemyPokemon.id && m.isTransferable, - false, - ) - .map(i => { - const ret = i as PokemonHeldItemModifier; - //@ts-expect-error - this is awful to fix/change - ret.pokemonId = null; - return ret; - }), - ); + this.postBattleLoot.push(...enemyPokemon.getHeldItems()); } pickUpScatteredMoney(): void { const moneyAmount = new NumberHolder(globalScene.currentBattle.moneyScattered); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) { moneyAmount.value *= 2; @@ -616,7 +605,7 @@ export const classicFixedBattles: FixedBattleConfigs = { ), ) .setCustomModifierRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], + guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.GREAT, RewardTier.GREAT], allowLuckUpgrades: false, }), [ClassicFixedBossWaves.EVIL_GRUNT_1]: new FixedBattleConfig() @@ -648,7 +637,7 @@ export const classicFixedBattles: FixedBattleConfigs = { ), ) .setCustomModifierRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], + guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.GREAT, RewardTier.GREAT], allowLuckUpgrades: false, }), [ClassicFixedBossWaves.EVIL_GRUNT_2]: new FixedBattleConfig() @@ -721,7 +710,7 @@ export const classicFixedBattles: FixedBattleConfigs = { ), ) .setCustomModifierRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], + guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.ULTRA], allowLuckUpgrades: false, }), [ClassicFixedBossWaves.EVIL_GRUNT_4]: new FixedBattleConfig() @@ -784,11 +773,11 @@ export const classicFixedBattles: FixedBattleConfigs = { ) .setCustomModifierRewards({ guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, + RewardTier.ROGUE, + RewardTier.ROGUE, + RewardTier.ULTRA, + RewardTier.ULTRA, + RewardTier.ULTRA, ], allowLuckUpgrades: false, }), @@ -803,11 +792,11 @@ export const classicFixedBattles: FixedBattleConfigs = { ) .setCustomModifierRewards({ guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, + RewardTier.ROGUE, + RewardTier.ROGUE, + RewardTier.ROGUE, + RewardTier.ULTRA, + RewardTier.ULTRA, ], allowLuckUpgrades: false, }), @@ -830,12 +819,12 @@ export const classicFixedBattles: FixedBattleConfigs = { ) .setCustomModifierRewards({ guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, + RewardTier.ROGUE, + RewardTier.ROGUE, + RewardTier.ULTRA, + RewardTier.ULTRA, + RewardTier.ULTRA, + RewardTier.ULTRA, ], allowLuckUpgrades: false, }), @@ -934,12 +923,12 @@ export const classicFixedBattles: FixedBattleConfigs = { ) .setCustomModifierRewards({ guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.GREAT, - ModifierTier.GREAT, + RewardTier.ROGUE, + RewardTier.ROGUE, + RewardTier.ULTRA, + RewardTier.ULTRA, + RewardTier.GREAT, + RewardTier.GREAT, ], allowLuckUpgrades: false, }), diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index fa4ee2286ff..8972c6e663d 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -22,7 +22,6 @@ import { Gender } from "#app/data/gender"; import { applyMoveAttrs } from "../moves/apply-attrs"; import { allMoves } from "../data-lists"; import { ArenaTagSide } from "#enums/arena-tag-side"; -import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { TerrainType } from "#app/data/terrain"; import { pokemonFormChanges } from "../pokemon-forms"; import { @@ -31,7 +30,6 @@ import { } from "../pokemon-forms/form-change-triggers"; import i18next from "i18next"; import { Command } from "#enums/command"; -import { BerryModifierType } from "#app/modifier/modifier-type"; import { getPokeballName } from "#app/data/pokeball"; import { BattleType } from "#enums/battle-type"; import { globalScene } from "#app/global-scene"; @@ -79,6 +77,9 @@ import type { BattlerIndex } from "#enums/battler-index"; import type Move from "#app/data/moves/move"; import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag"; import type { Constructor } from "#app/utils/common"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; +import { type BerryHeldItem, berryTypeToHeldItem } from "#app/items/held-items/berry"; import type { Localizable } from "#app/@types/locales"; import { applyAbAttrs } from "./apply-ab-attrs"; import type { Closed, Exact } from "#app/@types/type-helpers"; @@ -2045,7 +2046,7 @@ export abstract class PostAttackAbAttr extends AbAttr { export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { private stealCondition: PokemonAttackCondition | null; - private stolenItem?: PokemonHeldItemModifier; + private stolenItem?: HeldItemId; constructor(stealCondition?: PokemonAttackCondition) { super(); @@ -2064,11 +2065,11 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { hitResult < HitResult.NO_EFFECT && (!this.stealCondition || this.stealCondition(pokemon, opponent, move)) ) { - const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); + const heldItems = opponent.heldItemManager.getTransferableHeldItems(); if (heldItems.length) { // Ensure that the stolen item in testing is the same as when the effect is applied this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; - if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { + if (globalScene.canTransferHeldItem(this.stolenItem, opponent, pokemon)) { return true; } } @@ -2078,28 +2079,21 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { } override apply({ opponent, pokemon }: PostMoveInteractionAbAttrParams): void { - const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); + const heldItems = opponent.heldItemManager.getTransferableHeldItems(); if (!this.stolenItem) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; } - if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { + if (globalScene.tryTransferHeldItem(this.stolenItem, opponent, pokemon, false)) { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postAttackStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), defenderName: opponent.name, - stolenItemType: this.stolenItem.type.name, + stolenItemType: allHeldItems[this.stolenItem].name, }), ); } this.stolenItem = undefined; } - - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === target.id, - target.isPlayer(), - ) as PokemonHeldItemModifier[]; - } } export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { @@ -2190,7 +2184,7 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr { export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { private condition?: PokemonDefendCondition; - private stolenItem?: PokemonHeldItemModifier; + private stolenItem?: HeldItemId; constructor(condition?: PokemonDefendCondition) { super(); @@ -2200,10 +2194,10 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { override canApply({ simulated, pokemon, opponent, move, hitResult }: PostMoveInteractionAbAttrParams): boolean { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, opponent, move))) { - const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); + const heldItems = opponent.heldItemManager.getTransferableHeldItems(); if (heldItems.length) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; - if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { + if (globalScene.canTransferHeldItem(this.stolenItem, opponent, pokemon)) { return true; } } @@ -2212,28 +2206,21 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { } override apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void { - const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); + const heldItems = opponent.heldItemManager.getTransferableHeldItems(); if (!this.stolenItem) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; } - if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { + if (globalScene.tryTransferHeldItem(this.stolenItem, opponent, pokemon, false)) { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postDefendStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), attackerName: opponent.name, - stolenItemType: this.stolenItem.type.name, + stolenItemType: allHeldItems[this.stolenItem].name, }), ); } this.stolenItem = undefined; } - - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === target.id, - target.isPlayer(), - ) as PokemonHeldItemModifier[]; - } } /** @@ -4590,10 +4577,14 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { override canApply({ pokemon }: AbAttrBaseParams): boolean { // Ensure we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped) const cappedBerries = new Set( - globalScene - .getModifiers(BerryModifier, pokemon.isPlayer()) - .filter(bm => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1) - .map(bm => bm.berryType), + pokemon + .getHeldItems() + .filter( + bm => + isItemInCategory(bm, HeldItemCategoryId.BERRY) && + pokemon.heldItemManager.getStack(bm) < allHeldItems[bm].maxStackCount, + ) + .map(bm => (allHeldItems[bm] as BerryHeldItem).berryType), ); this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(bt => !cappedBerries.has(bt)); @@ -4623,30 +4614,15 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { const randomIdx = randSeedInt(this.berriesUnderCap.length); const chosenBerryType = this.berriesUnderCap[randomIdx]; pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory - const chosenBerry = new BerryModifierType(chosenBerryType); + const chosenBerry = berryTypeToHeldItem[chosenBerryType]; - // Add the randomly chosen berry or update the existing one - const berryModifier = globalScene.findModifier( - m => m instanceof BerryModifier && m.berryType === chosenBerryType && m.pokemonId === pokemon.id, - pokemon.isPlayer(), - ) as BerryModifier | undefined; + pokemon.heldItemManager.add(chosenBerry); - if (berryModifier) { - berryModifier.stackCount++; - } else { - const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1); - if (pokemon.isPlayer()) { - globalScene.addModifier(newBerry); - } else { - globalScene.addEnemyModifier(newBerry); - } - } - - globalScene.updateModifiers(pokemon.isPlayer()); + globalScene.updateItems(pokemon.isPlayer()); globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - berryName: chosenBerry.name, + berryName: allHeldItems[chosenBerry].name, }), ); return true; @@ -4680,8 +4656,7 @@ export class CudChewConsumeBerryAbAttr extends AbAttr { // This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed. for (const berryType of pokemon.summonData.berriesEatenLast) { getBerryEffectFunc(berryType)(pokemon); - const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1); - globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message + globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(pokemon, berryType)); // trigger message } // uncomment to make cheek pouch work with cud chew @@ -5266,13 +5241,13 @@ export abstract class PostBattleAbAttr extends AbAttr { } export class PostBattleLootAbAttr extends PostBattleAbAttr { - private randItem?: PokemonHeldItemModifier; + private randItem?: HeldItemId; override canApply({ simulated, victory, pokemon }: PostBattleAbAttrParams): boolean { const postBattleLoot = globalScene.currentBattle.postBattleLoot; if (!simulated && postBattleLoot.length && victory) { this.randItem = randSeedItem(postBattleLoot); - return globalScene.canTransferHeldItemModifier(this.randItem, pokemon, 1); + return pokemon.heldItemManager.getStack(this.randItem) < allHeldItems[this.randItem].maxStackCount; } return false; } @@ -5283,12 +5258,12 @@ export class PostBattleLootAbAttr extends PostBattleAbAttr { this.randItem = randSeedItem(postBattleLoot); } - if (globalScene.tryTransferHeldItemModifier(this.randItem, pokemon, true, 1, true, undefined, false)) { + if (pokemon.heldItemManager.add(this.randItem)) { postBattleLoot.splice(postBattleLoot.indexOf(this.randItem), 1); globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postBattleLoot", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - itemName: this.randItem.type.name, + itemName: allHeldItems[this.randItem].name, }), ); } @@ -6187,8 +6162,6 @@ class ForceSwitchOutHelper { } if (!allyPokemon?.isActive(true)) { - globalScene.clearEnemyHeldItemModifiers(); - if (switchOutTarget.hp) { globalScene.phaseManager.pushNew("BattleEndPhase", false); @@ -6272,11 +6245,9 @@ class ForceSwitchOutHelper { * @returns The amount of health recovered by Shell Bell. */ function calculateShellBellRecovery(pokemon: Pokemon): number { - const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); - if (shellBellModifier) { - return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount; - } - return 0; + // Returns 0 if no Shell Bell is present + const shellBellStack = pokemon.heldItemManager.getStack(HeldItemId.SHELL_BELL); + return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellStack; } export interface PostDamageAbAttrParams extends AbAttrBaseParams { diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 8714d6c0c9a..fb3d0e119bf 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -11,11 +11,11 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { SpeciesFormKey } from "#enums/species-form-key"; import { TimeOfDay } from "#enums/time-of-day"; -import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#app/modifier/modifier-type"; import { speciesStarterCosts } from "./starters"; import i18next from "i18next"; import { allMoves } from "#app/data/data-lists"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; +import { HeldItemId } from "#enums/held-item-id"; export enum SpeciesWildEvolutionDelay { NONE, @@ -99,7 +99,7 @@ const EvoCondKey = { SPECIES_CAUGHT: 12, GENDER: 13, NATURE: 14, - HELD_ITEM: 15, // Currently checks only for species stat booster items + HELD_ITEM: 15, } as const; type EvolutionConditionData = @@ -110,7 +110,7 @@ type EvolutionConditionData = {key: typeof EvoCondKey.GENDER, gender: Gender} | {key: typeof EvoCondKey.MOVE_TYPE | typeof EvoCondKey.PARTY_TYPE, pkmnType: PokemonType} | {key: typeof EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId} | - {key: typeof EvoCondKey.HELD_ITEM, itemKey: SpeciesStatBoosterItem} | + {key: typeof EvoCondKey.HELD_ITEM, itemKey: HeldItemId} | {key: typeof EvoCondKey.NATURE, nature: Nature[]} | {key: typeof EvoCondKey.WEATHER, weather: WeatherType[]} | {key: typeof EvoCondKey.TYROGUE, move: TyrogueMove} | @@ -201,7 +201,7 @@ export class SpeciesEvolutionCondition { case EvoCondKey.SPECIES_CAUGHT: return !!globalScene.gameData.dexData[cond.speciesCaught].caughtAttr; case EvoCondKey.HELD_ITEM: - return pokemon.getHeldItems().some(m => m.is("SpeciesStatBoosterModifier") && (m.type as SpeciesStatBoosterModifierType).key === cond.itemKey) + return pokemon.heldItemManager.hasItem(cond.itemKey); } }); } @@ -1765,8 +1765,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.DUSKNOIR, 1, EvolutionItem.REAPER_CLOTH, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.CLAMPERL]: [ - new SpeciesEvolution(SpeciesId.HUNTAIL, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: "DEEP_SEA_TOOTH"}, SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesEvolution(SpeciesId.GOREBYSS, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: "DEEP_SEA_SCALE"}, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.HUNTAIL, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: HeldItemId.DEEP_SEA_TOOTH}, SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesEvolution(SpeciesId.GOREBYSS, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: HeldItemId.DEEP_SEA_SCALE}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.BOLDORE]: [ new SpeciesEvolution(SpeciesId.GIGALITH, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) diff --git a/src/data/balance/tms.ts b/src/data/balance/tms.ts index e194dc4040c..b6317e8edd4 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -1,4 +1,4 @@ -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -68591,324 +68591,324 @@ function transposeTmSpecies(): SpeciesTmMoves { export const speciesTmMoves: SpeciesTmMoves = transposeTmSpecies(); interface TmPoolTiers { - [key: number]: ModifierTier + [key: number]: RewardTier } export const tmPoolTiers: TmPoolTiers = { - [MoveId.MEGA_PUNCH]: ModifierTier.GREAT, - [MoveId.PAY_DAY]: ModifierTier.ULTRA, - [MoveId.FIRE_PUNCH]: ModifierTier.GREAT, - [MoveId.ICE_PUNCH]: ModifierTier.GREAT, - [MoveId.THUNDER_PUNCH]: ModifierTier.GREAT, - [MoveId.SWORDS_DANCE]: ModifierTier.COMMON, - [MoveId.CUT]: ModifierTier.COMMON, - [MoveId.FLY]: ModifierTier.COMMON, - [MoveId.MEGA_KICK]: ModifierTier.GREAT, - [MoveId.BODY_SLAM]: ModifierTier.GREAT, - [MoveId.TAKE_DOWN]: ModifierTier.GREAT, - [MoveId.DOUBLE_EDGE]: ModifierTier.ULTRA, - [MoveId.PIN_MISSILE]: ModifierTier.COMMON, - [MoveId.ROAR]: ModifierTier.COMMON, - [MoveId.FLAMETHROWER]: ModifierTier.ULTRA, - [MoveId.HYDRO_PUMP]: ModifierTier.ULTRA, - [MoveId.SURF]: ModifierTier.ULTRA, - [MoveId.ICE_BEAM]: ModifierTier.ULTRA, - [MoveId.BLIZZARD]: ModifierTier.ULTRA, - [MoveId.PSYBEAM]: ModifierTier.GREAT, - [MoveId.HYPER_BEAM]: ModifierTier.ULTRA, - [MoveId.LOW_KICK]: ModifierTier.COMMON, - [MoveId.COUNTER]: ModifierTier.COMMON, - [MoveId.STRENGTH]: ModifierTier.GREAT, - [MoveId.SOLAR_BEAM]: ModifierTier.ULTRA, - [MoveId.FIRE_SPIN]: ModifierTier.COMMON, - [MoveId.THUNDERBOLT]: ModifierTier.ULTRA, - [MoveId.THUNDER_WAVE]: ModifierTier.COMMON, - [MoveId.THUNDER]: ModifierTier.ULTRA, - [MoveId.EARTHQUAKE]: ModifierTier.ULTRA, - [MoveId.DIG]: ModifierTier.GREAT, - [MoveId.TOXIC]: ModifierTier.GREAT, - [MoveId.PSYCHIC]: ModifierTier.ULTRA, - [MoveId.AGILITY]: ModifierTier.COMMON, - [MoveId.NIGHT_SHADE]: ModifierTier.COMMON, - [MoveId.SCREECH]: ModifierTier.COMMON, - [MoveId.DOUBLE_TEAM]: ModifierTier.COMMON, - [MoveId.CONFUSE_RAY]: ModifierTier.COMMON, - [MoveId.LIGHT_SCREEN]: ModifierTier.COMMON, - [MoveId.HAZE]: ModifierTier.COMMON, - [MoveId.REFLECT]: ModifierTier.COMMON, - [MoveId.FOCUS_ENERGY]: ModifierTier.COMMON, - [MoveId.METRONOME]: ModifierTier.COMMON, - [MoveId.SELF_DESTRUCT]: ModifierTier.GREAT, - [MoveId.FIRE_BLAST]: ModifierTier.ULTRA, - [MoveId.WATERFALL]: ModifierTier.GREAT, - [MoveId.SWIFT]: ModifierTier.COMMON, - [MoveId.AMNESIA]: ModifierTier.COMMON, - [MoveId.DREAM_EATER]: ModifierTier.GREAT, - [MoveId.LEECH_LIFE]: ModifierTier.ULTRA, - [MoveId.FLASH]: ModifierTier.COMMON, - [MoveId.EXPLOSION]: ModifierTier.GREAT, - [MoveId.REST]: ModifierTier.COMMON, - [MoveId.ROCK_SLIDE]: ModifierTier.GREAT, - [MoveId.TRI_ATTACK]: ModifierTier.ULTRA, - [MoveId.SUPER_FANG]: ModifierTier.COMMON, - [MoveId.SUBSTITUTE]: ModifierTier.COMMON, - [MoveId.THIEF]: ModifierTier.GREAT, - [MoveId.SNORE]: ModifierTier.COMMON, - [MoveId.CURSE]: ModifierTier.COMMON, - [MoveId.REVERSAL]: ModifierTier.COMMON, - [MoveId.SPITE]: ModifierTier.COMMON, - [MoveId.PROTECT]: ModifierTier.COMMON, - [MoveId.SCARY_FACE]: ModifierTier.COMMON, - [MoveId.SLUDGE_BOMB]: ModifierTier.GREAT, - [MoveId.MUD_SLAP]: ModifierTier.COMMON, - [MoveId.SPIKES]: ModifierTier.COMMON, - [MoveId.ICY_WIND]: ModifierTier.GREAT, - [MoveId.OUTRAGE]: ModifierTier.ULTRA, - [MoveId.SANDSTORM]: ModifierTier.COMMON, - [MoveId.GIGA_DRAIN]: ModifierTier.ULTRA, - [MoveId.ENDURE]: ModifierTier.COMMON, - [MoveId.CHARM]: ModifierTier.COMMON, - [MoveId.FALSE_SWIPE]: ModifierTier.COMMON, - [MoveId.SWAGGER]: ModifierTier.COMMON, - [MoveId.STEEL_WING]: ModifierTier.GREAT, - [MoveId.ATTRACT]: ModifierTier.COMMON, - [MoveId.SLEEP_TALK]: ModifierTier.COMMON, - [MoveId.HEAL_BELL]: ModifierTier.COMMON, - [MoveId.RETURN]: ModifierTier.ULTRA, - [MoveId.FRUSTRATION]: ModifierTier.COMMON, - [MoveId.SAFEGUARD]: ModifierTier.COMMON, - [MoveId.PAIN_SPLIT]: ModifierTier.COMMON, - [MoveId.MEGAHORN]: ModifierTier.ULTRA, - [MoveId.BATON_PASS]: ModifierTier.COMMON, - [MoveId.ENCORE]: ModifierTier.COMMON, - [MoveId.IRON_TAIL]: ModifierTier.GREAT, - [MoveId.METAL_CLAW]: ModifierTier.COMMON, - [MoveId.SYNTHESIS]: ModifierTier.GREAT, - [MoveId.HIDDEN_POWER]: ModifierTier.GREAT, - [MoveId.RAIN_DANCE]: ModifierTier.COMMON, - [MoveId.SUNNY_DAY]: ModifierTier.COMMON, - [MoveId.CRUNCH]: ModifierTier.GREAT, - [MoveId.PSYCH_UP]: ModifierTier.COMMON, - [MoveId.SHADOW_BALL]: ModifierTier.ULTRA, - [MoveId.FUTURE_SIGHT]: ModifierTier.GREAT, - [MoveId.ROCK_SMASH]: ModifierTier.COMMON, - [MoveId.WHIRLPOOL]: ModifierTier.COMMON, - [MoveId.BEAT_UP]: ModifierTier.COMMON, - [MoveId.UPROAR]: ModifierTier.GREAT, - [MoveId.HEAT_WAVE]: ModifierTier.ULTRA, - [MoveId.HAIL]: ModifierTier.COMMON, - [MoveId.TORMENT]: ModifierTier.COMMON, - [MoveId.WILL_O_WISP]: ModifierTier.COMMON, - [MoveId.FACADE]: ModifierTier.GREAT, - [MoveId.FOCUS_PUNCH]: ModifierTier.COMMON, - [MoveId.NATURE_POWER]: ModifierTier.COMMON, - [MoveId.CHARGE]: ModifierTier.COMMON, - [MoveId.TAUNT]: ModifierTier.COMMON, - [MoveId.HELPING_HAND]: ModifierTier.COMMON, - [MoveId.TRICK]: ModifierTier.COMMON, - [MoveId.SUPERPOWER]: ModifierTier.ULTRA, - [MoveId.RECYCLE]: ModifierTier.COMMON, - [MoveId.REVENGE]: ModifierTier.GREAT, - [MoveId.BRICK_BREAK]: ModifierTier.GREAT, - [MoveId.KNOCK_OFF]: ModifierTier.GREAT, - [MoveId.ENDEAVOR]: ModifierTier.COMMON, - [MoveId.SKILL_SWAP]: ModifierTier.COMMON, - [MoveId.IMPRISON]: ModifierTier.COMMON, - [MoveId.SECRET_POWER]: ModifierTier.COMMON, - [MoveId.DIVE]: ModifierTier.GREAT, - [MoveId.FEATHER_DANCE]: ModifierTier.COMMON, - [MoveId.BLAZE_KICK]: ModifierTier.GREAT, - [MoveId.HYPER_VOICE]: ModifierTier.ULTRA, - [MoveId.BLAST_BURN]: ModifierTier.ULTRA, - [MoveId.HYDRO_CANNON]: ModifierTier.ULTRA, - [MoveId.WEATHER_BALL]: ModifierTier.COMMON, - [MoveId.FAKE_TEARS]: ModifierTier.COMMON, - [MoveId.AIR_CUTTER]: ModifierTier.GREAT, - [MoveId.OVERHEAT]: ModifierTier.ULTRA, - [MoveId.ROCK_TOMB]: ModifierTier.GREAT, - [MoveId.METAL_SOUND]: ModifierTier.COMMON, - [MoveId.COSMIC_POWER]: ModifierTier.COMMON, - [MoveId.SIGNAL_BEAM]: ModifierTier.GREAT, - [MoveId.SAND_TOMB]: ModifierTier.COMMON, - [MoveId.MUDDY_WATER]: ModifierTier.GREAT, - [MoveId.BULLET_SEED]: ModifierTier.GREAT, - [MoveId.AERIAL_ACE]: ModifierTier.GREAT, - [MoveId.ICICLE_SPEAR]: ModifierTier.GREAT, - [MoveId.IRON_DEFENSE]: ModifierTier.GREAT, - [MoveId.DRAGON_CLAW]: ModifierTier.ULTRA, - [MoveId.FRENZY_PLANT]: ModifierTier.ULTRA, - [MoveId.BULK_UP]: ModifierTier.COMMON, - [MoveId.BOUNCE]: ModifierTier.GREAT, - [MoveId.MUD_SHOT]: ModifierTier.GREAT, - [MoveId.POISON_TAIL]: ModifierTier.GREAT, - [MoveId.COVET]: ModifierTier.GREAT, - [MoveId.MAGICAL_LEAF]: ModifierTier.GREAT, - [MoveId.CALM_MIND]: ModifierTier.GREAT, - [MoveId.LEAF_BLADE]: ModifierTier.ULTRA, - [MoveId.DRAGON_DANCE]: ModifierTier.GREAT, - [MoveId.ROCK_BLAST]: ModifierTier.GREAT, - [MoveId.WATER_PULSE]: ModifierTier.GREAT, - [MoveId.ROOST]: ModifierTier.GREAT, - [MoveId.GRAVITY]: ModifierTier.COMMON, - [MoveId.GYRO_BALL]: ModifierTier.COMMON, - [MoveId.BRINE]: ModifierTier.GREAT, - [MoveId.PLUCK]: ModifierTier.GREAT, - [MoveId.TAILWIND]: ModifierTier.GREAT, - [MoveId.U_TURN]: ModifierTier.GREAT, - [MoveId.CLOSE_COMBAT]: ModifierTier.ULTRA, - [MoveId.PAYBACK]: ModifierTier.COMMON, - [MoveId.ASSURANCE]: ModifierTier.COMMON, - [MoveId.EMBARGO]: ModifierTier.COMMON, - [MoveId.FLING]: ModifierTier.COMMON, - [MoveId.GASTRO_ACID]: ModifierTier.GREAT, - [MoveId.POWER_SWAP]: ModifierTier.COMMON, - [MoveId.GUARD_SWAP]: ModifierTier.COMMON, - [MoveId.WORRY_SEED]: ModifierTier.GREAT, - [MoveId.TOXIC_SPIKES]: ModifierTier.GREAT, - [MoveId.FLARE_BLITZ]: ModifierTier.ULTRA, - [MoveId.AURA_SPHERE]: ModifierTier.GREAT, - [MoveId.ROCK_POLISH]: ModifierTier.COMMON, - [MoveId.POISON_JAB]: ModifierTier.GREAT, - [MoveId.DARK_PULSE]: ModifierTier.GREAT, - [MoveId.AQUA_TAIL]: ModifierTier.GREAT, - [MoveId.SEED_BOMB]: ModifierTier.GREAT, - [MoveId.AIR_SLASH]: ModifierTier.GREAT, - [MoveId.X_SCISSOR]: ModifierTier.GREAT, - [MoveId.BUG_BUZZ]: ModifierTier.GREAT, - [MoveId.DRAGON_PULSE]: ModifierTier.GREAT, - [MoveId.POWER_GEM]: ModifierTier.GREAT, - [MoveId.DRAIN_PUNCH]: ModifierTier.GREAT, - [MoveId.VACUUM_WAVE]: ModifierTier.COMMON, - [MoveId.FOCUS_BLAST]: ModifierTier.GREAT, - [MoveId.ENERGY_BALL]: ModifierTier.GREAT, - [MoveId.BRAVE_BIRD]: ModifierTier.ULTRA, - [MoveId.EARTH_POWER]: ModifierTier.ULTRA, - [MoveId.GIGA_IMPACT]: ModifierTier.GREAT, - [MoveId.NASTY_PLOT]: ModifierTier.COMMON, - [MoveId.AVALANCHE]: ModifierTier.GREAT, - [MoveId.SHADOW_CLAW]: ModifierTier.GREAT, - [MoveId.THUNDER_FANG]: ModifierTier.GREAT, - [MoveId.ICE_FANG]: ModifierTier.GREAT, - [MoveId.FIRE_FANG]: ModifierTier.GREAT, - [MoveId.PSYCHO_CUT]: ModifierTier.GREAT, - [MoveId.ZEN_HEADBUTT]: ModifierTier.GREAT, - [MoveId.FLASH_CANNON]: ModifierTier.GREAT, - [MoveId.ROCK_CLIMB]: ModifierTier.GREAT, - [MoveId.DEFOG]: ModifierTier.COMMON, - [MoveId.TRICK_ROOM]: ModifierTier.COMMON, - [MoveId.DRACO_METEOR]: ModifierTier.ULTRA, - [MoveId.LEAF_STORM]: ModifierTier.ULTRA, - [MoveId.POWER_WHIP]: ModifierTier.ULTRA, - [MoveId.CROSS_POISON]: ModifierTier.GREAT, - [MoveId.GUNK_SHOT]: ModifierTier.ULTRA, - [MoveId.IRON_HEAD]: ModifierTier.GREAT, - [MoveId.STONE_EDGE]: ModifierTier.ULTRA, - [MoveId.STEALTH_ROCK]: ModifierTier.COMMON, - [MoveId.GRASS_KNOT]: ModifierTier.ULTRA, - [MoveId.BUG_BITE]: ModifierTier.GREAT, - [MoveId.CHARGE_BEAM]: ModifierTier.GREAT, - [MoveId.HONE_CLAWS]: ModifierTier.COMMON, - [MoveId.WONDER_ROOM]: ModifierTier.COMMON, - [MoveId.PSYSHOCK]: ModifierTier.GREAT, - [MoveId.VENOSHOCK]: ModifierTier.GREAT, - [MoveId.MAGIC_ROOM]: ModifierTier.COMMON, - [MoveId.SMACK_DOWN]: ModifierTier.COMMON, - [MoveId.SLUDGE_WAVE]: ModifierTier.GREAT, - [MoveId.HEAVY_SLAM]: ModifierTier.GREAT, - [MoveId.ELECTRO_BALL]: ModifierTier.GREAT, - [MoveId.FLAME_CHARGE]: ModifierTier.GREAT, - [MoveId.LOW_SWEEP]: ModifierTier.GREAT, - [MoveId.ACID_SPRAY]: ModifierTier.COMMON, - [MoveId.FOUL_PLAY]: ModifierTier.ULTRA, - [MoveId.ROUND]: ModifierTier.COMMON, - [MoveId.ECHOED_VOICE]: ModifierTier.COMMON, - [MoveId.STORED_POWER]: ModifierTier.COMMON, - [MoveId.ALLY_SWITCH]: ModifierTier.COMMON, - [MoveId.SCALD]: ModifierTier.GREAT, - [MoveId.HEX]: ModifierTier.GREAT, - [MoveId.SKY_DROP]: ModifierTier.GREAT, - [MoveId.INCINERATE]: ModifierTier.GREAT, - [MoveId.QUASH]: ModifierTier.COMMON, - [MoveId.ACROBATICS]: ModifierTier.GREAT, - [MoveId.RETALIATE]: ModifierTier.GREAT, - [MoveId.WATER_PLEDGE]: ModifierTier.GREAT, - [MoveId.FIRE_PLEDGE]: ModifierTier.GREAT, - [MoveId.GRASS_PLEDGE]: ModifierTier.GREAT, - [MoveId.VOLT_SWITCH]: ModifierTier.GREAT, - [MoveId.STRUGGLE_BUG]: ModifierTier.COMMON, - [MoveId.BULLDOZE]: ModifierTier.GREAT, - [MoveId.FROST_BREATH]: ModifierTier.GREAT, - [MoveId.DRAGON_TAIL]: ModifierTier.GREAT, - [MoveId.WORK_UP]: ModifierTier.COMMON, - [MoveId.ELECTROWEB]: ModifierTier.GREAT, - [MoveId.WILD_CHARGE]: ModifierTier.GREAT, - [MoveId.DRILL_RUN]: ModifierTier.GREAT, - [MoveId.RAZOR_SHELL]: ModifierTier.GREAT, - [MoveId.HEAT_CRASH]: ModifierTier.GREAT, - [MoveId.TAIL_SLAP]: ModifierTier.GREAT, - [MoveId.HURRICANE]: ModifierTier.ULTRA, - [MoveId.SNARL]: ModifierTier.COMMON, - [MoveId.PHANTOM_FORCE]: ModifierTier.ULTRA, - [MoveId.PETAL_BLIZZARD]: ModifierTier.GREAT, - [MoveId.DISARMING_VOICE]: ModifierTier.GREAT, - [MoveId.DRAINING_KISS]: ModifierTier.GREAT, - [MoveId.GRASSY_TERRAIN]: ModifierTier.COMMON, - [MoveId.MISTY_TERRAIN]: ModifierTier.COMMON, - [MoveId.PLAY_ROUGH]: ModifierTier.GREAT, - [MoveId.CONFIDE]: ModifierTier.COMMON, - [MoveId.MYSTICAL_FIRE]: ModifierTier.GREAT, - [MoveId.EERIE_IMPULSE]: ModifierTier.COMMON, - [MoveId.VENOM_DRENCH]: ModifierTier.COMMON, - [MoveId.ELECTRIC_TERRAIN]: ModifierTier.COMMON, - [MoveId.DAZZLING_GLEAM]: ModifierTier.ULTRA, - [MoveId.INFESTATION]: ModifierTier.COMMON, - [MoveId.POWER_UP_PUNCH]: ModifierTier.GREAT, - [MoveId.DARKEST_LARIAT]: ModifierTier.GREAT, - [MoveId.HIGH_HORSEPOWER]: ModifierTier.ULTRA, - [MoveId.SOLAR_BLADE]: ModifierTier.GREAT, - [MoveId.THROAT_CHOP]: ModifierTier.GREAT, - [MoveId.POLLEN_PUFF]: ModifierTier.GREAT, - [MoveId.PSYCHIC_TERRAIN]: ModifierTier.COMMON, - [MoveId.LUNGE]: ModifierTier.GREAT, - [MoveId.SPEED_SWAP]: ModifierTier.COMMON, - [MoveId.SMART_STRIKE]: ModifierTier.GREAT, - [MoveId.BRUTAL_SWING]: ModifierTier.GREAT, - [MoveId.AURORA_VEIL]: ModifierTier.COMMON, - [MoveId.PSYCHIC_FANGS]: ModifierTier.GREAT, - [MoveId.STOMPING_TANTRUM]: ModifierTier.GREAT, - [MoveId.LIQUIDATION]: ModifierTier.ULTRA, - [MoveId.BODY_PRESS]: ModifierTier.ULTRA, - [MoveId.BREAKING_SWIPE]: ModifierTier.GREAT, - [MoveId.STEEL_BEAM]: ModifierTier.ULTRA, - [MoveId.EXPANDING_FORCE]: ModifierTier.GREAT, - [MoveId.STEEL_ROLLER]: ModifierTier.COMMON, - [MoveId.SCALE_SHOT]: ModifierTier.ULTRA, - [MoveId.METEOR_BEAM]: ModifierTier.GREAT, - [MoveId.MISTY_EXPLOSION]: ModifierTier.COMMON, - [MoveId.GRASSY_GLIDE]: ModifierTier.COMMON, - [MoveId.RISING_VOLTAGE]: ModifierTier.COMMON, - [MoveId.TERRAIN_PULSE]: ModifierTier.COMMON, - [MoveId.SKITTER_SMACK]: ModifierTier.GREAT, - [MoveId.BURNING_JEALOUSY]: ModifierTier.GREAT, - [MoveId.LASH_OUT]: ModifierTier.GREAT, - [MoveId.POLTERGEIST]: ModifierTier.ULTRA, - [MoveId.CORROSIVE_GAS]: ModifierTier.COMMON, - [MoveId.COACHING]: ModifierTier.COMMON, - [MoveId.FLIP_TURN]: ModifierTier.COMMON, - [MoveId.TRIPLE_AXEL]: ModifierTier.COMMON, - [MoveId.DUAL_WINGBEAT]: ModifierTier.COMMON, - [MoveId.SCORCHING_SANDS]: ModifierTier.GREAT, - [MoveId.TERA_BLAST]: ModifierTier.GREAT, - [MoveId.ICE_SPINNER]: ModifierTier.GREAT, - [MoveId.SNOWSCAPE]: ModifierTier.COMMON, - [MoveId.POUNCE]: ModifierTier.COMMON, - [MoveId.TRAILBLAZE]: ModifierTier.COMMON, - [MoveId.CHILLING_WATER]: ModifierTier.COMMON, - [MoveId.HARD_PRESS]: ModifierTier.GREAT, - [MoveId.DRAGON_CHEER]: ModifierTier.COMMON, - [MoveId.ALLURING_VOICE]: ModifierTier.GREAT, - [MoveId.TEMPER_FLARE]: ModifierTier.GREAT, - [MoveId.SUPERCELL_SLAM]: ModifierTier.GREAT, - [MoveId.PSYCHIC_NOISE]: ModifierTier.GREAT, - [MoveId.UPPER_HAND]: ModifierTier.COMMON, + [MoveId.MEGA_PUNCH]: RewardTier.GREAT, + [MoveId.PAY_DAY]: RewardTier.ULTRA, + [MoveId.FIRE_PUNCH]: RewardTier.GREAT, + [MoveId.ICE_PUNCH]: RewardTier.GREAT, + [MoveId.THUNDER_PUNCH]: RewardTier.GREAT, + [MoveId.SWORDS_DANCE]: RewardTier.COMMON, + [MoveId.CUT]: RewardTier.COMMON, + [MoveId.FLY]: RewardTier.COMMON, + [MoveId.MEGA_KICK]: RewardTier.GREAT, + [MoveId.BODY_SLAM]: RewardTier.GREAT, + [MoveId.TAKE_DOWN]: RewardTier.GREAT, + [MoveId.DOUBLE_EDGE]: RewardTier.ULTRA, + [MoveId.PIN_MISSILE]: RewardTier.COMMON, + [MoveId.ROAR]: RewardTier.COMMON, + [MoveId.FLAMETHROWER]: RewardTier.ULTRA, + [MoveId.HYDRO_PUMP]: RewardTier.ULTRA, + [MoveId.SURF]: RewardTier.ULTRA, + [MoveId.ICE_BEAM]: RewardTier.ULTRA, + [MoveId.BLIZZARD]: RewardTier.ULTRA, + [MoveId.PSYBEAM]: RewardTier.GREAT, + [MoveId.HYPER_BEAM]: RewardTier.ULTRA, + [MoveId.LOW_KICK]: RewardTier.COMMON, + [MoveId.COUNTER]: RewardTier.COMMON, + [MoveId.STRENGTH]: RewardTier.GREAT, + [MoveId.SOLAR_BEAM]: RewardTier.ULTRA, + [MoveId.FIRE_SPIN]: RewardTier.COMMON, + [MoveId.THUNDERBOLT]: RewardTier.ULTRA, + [MoveId.THUNDER_WAVE]: RewardTier.COMMON, + [MoveId.THUNDER]: RewardTier.ULTRA, + [MoveId.EARTHQUAKE]: RewardTier.ULTRA, + [MoveId.DIG]: RewardTier.GREAT, + [MoveId.TOXIC]: RewardTier.GREAT, + [MoveId.PSYCHIC]: RewardTier.ULTRA, + [MoveId.AGILITY]: RewardTier.COMMON, + [MoveId.NIGHT_SHADE]: RewardTier.COMMON, + [MoveId.SCREECH]: RewardTier.COMMON, + [MoveId.DOUBLE_TEAM]: RewardTier.COMMON, + [MoveId.CONFUSE_RAY]: RewardTier.COMMON, + [MoveId.LIGHT_SCREEN]: RewardTier.COMMON, + [MoveId.HAZE]: RewardTier.COMMON, + [MoveId.REFLECT]: RewardTier.COMMON, + [MoveId.FOCUS_ENERGY]: RewardTier.COMMON, + [MoveId.METRONOME]: RewardTier.COMMON, + [MoveId.SELF_DESTRUCT]: RewardTier.GREAT, + [MoveId.FIRE_BLAST]: RewardTier.ULTRA, + [MoveId.WATERFALL]: RewardTier.GREAT, + [MoveId.SWIFT]: RewardTier.COMMON, + [MoveId.AMNESIA]: RewardTier.COMMON, + [MoveId.DREAM_EATER]: RewardTier.GREAT, + [MoveId.LEECH_LIFE]: RewardTier.ULTRA, + [MoveId.FLASH]: RewardTier.COMMON, + [MoveId.EXPLOSION]: RewardTier.GREAT, + [MoveId.REST]: RewardTier.COMMON, + [MoveId.ROCK_SLIDE]: RewardTier.GREAT, + [MoveId.TRI_ATTACK]: RewardTier.ULTRA, + [MoveId.SUPER_FANG]: RewardTier.COMMON, + [MoveId.SUBSTITUTE]: RewardTier.COMMON, + [MoveId.THIEF]: RewardTier.GREAT, + [MoveId.SNORE]: RewardTier.COMMON, + [MoveId.CURSE]: RewardTier.COMMON, + [MoveId.REVERSAL]: RewardTier.COMMON, + [MoveId.SPITE]: RewardTier.COMMON, + [MoveId.PROTECT]: RewardTier.COMMON, + [MoveId.SCARY_FACE]: RewardTier.COMMON, + [MoveId.SLUDGE_BOMB]: RewardTier.GREAT, + [MoveId.MUD_SLAP]: RewardTier.COMMON, + [MoveId.SPIKES]: RewardTier.COMMON, + [MoveId.ICY_WIND]: RewardTier.GREAT, + [MoveId.OUTRAGE]: RewardTier.ULTRA, + [MoveId.SANDSTORM]: RewardTier.COMMON, + [MoveId.GIGA_DRAIN]: RewardTier.ULTRA, + [MoveId.ENDURE]: RewardTier.COMMON, + [MoveId.CHARM]: RewardTier.COMMON, + [MoveId.FALSE_SWIPE]: RewardTier.COMMON, + [MoveId.SWAGGER]: RewardTier.COMMON, + [MoveId.STEEL_WING]: RewardTier.GREAT, + [MoveId.ATTRACT]: RewardTier.COMMON, + [MoveId.SLEEP_TALK]: RewardTier.COMMON, + [MoveId.HEAL_BELL]: RewardTier.COMMON, + [MoveId.RETURN]: RewardTier.ULTRA, + [MoveId.FRUSTRATION]: RewardTier.COMMON, + [MoveId.SAFEGUARD]: RewardTier.COMMON, + [MoveId.PAIN_SPLIT]: RewardTier.COMMON, + [MoveId.MEGAHORN]: RewardTier.ULTRA, + [MoveId.BATON_PASS]: RewardTier.COMMON, + [MoveId.ENCORE]: RewardTier.COMMON, + [MoveId.IRON_TAIL]: RewardTier.GREAT, + [MoveId.METAL_CLAW]: RewardTier.COMMON, + [MoveId.SYNTHESIS]: RewardTier.GREAT, + [MoveId.HIDDEN_POWER]: RewardTier.GREAT, + [MoveId.RAIN_DANCE]: RewardTier.COMMON, + [MoveId.SUNNY_DAY]: RewardTier.COMMON, + [MoveId.CRUNCH]: RewardTier.GREAT, + [MoveId.PSYCH_UP]: RewardTier.COMMON, + [MoveId.SHADOW_BALL]: RewardTier.ULTRA, + [MoveId.FUTURE_SIGHT]: RewardTier.GREAT, + [MoveId.ROCK_SMASH]: RewardTier.COMMON, + [MoveId.WHIRLPOOL]: RewardTier.COMMON, + [MoveId.BEAT_UP]: RewardTier.COMMON, + [MoveId.UPROAR]: RewardTier.GREAT, + [MoveId.HEAT_WAVE]: RewardTier.ULTRA, + [MoveId.HAIL]: RewardTier.COMMON, + [MoveId.TORMENT]: RewardTier.COMMON, + [MoveId.WILL_O_WISP]: RewardTier.COMMON, + [MoveId.FACADE]: RewardTier.GREAT, + [MoveId.FOCUS_PUNCH]: RewardTier.COMMON, + [MoveId.NATURE_POWER]: RewardTier.COMMON, + [MoveId.CHARGE]: RewardTier.COMMON, + [MoveId.TAUNT]: RewardTier.COMMON, + [MoveId.HELPING_HAND]: RewardTier.COMMON, + [MoveId.TRICK]: RewardTier.COMMON, + [MoveId.SUPERPOWER]: RewardTier.ULTRA, + [MoveId.RECYCLE]: RewardTier.COMMON, + [MoveId.REVENGE]: RewardTier.GREAT, + [MoveId.BRICK_BREAK]: RewardTier.GREAT, + [MoveId.KNOCK_OFF]: RewardTier.GREAT, + [MoveId.ENDEAVOR]: RewardTier.COMMON, + [MoveId.SKILL_SWAP]: RewardTier.COMMON, + [MoveId.IMPRISON]: RewardTier.COMMON, + [MoveId.SECRET_POWER]: RewardTier.COMMON, + [MoveId.DIVE]: RewardTier.GREAT, + [MoveId.FEATHER_DANCE]: RewardTier.COMMON, + [MoveId.BLAZE_KICK]: RewardTier.GREAT, + [MoveId.HYPER_VOICE]: RewardTier.ULTRA, + [MoveId.BLAST_BURN]: RewardTier.ULTRA, + [MoveId.HYDRO_CANNON]: RewardTier.ULTRA, + [MoveId.WEATHER_BALL]: RewardTier.COMMON, + [MoveId.FAKE_TEARS]: RewardTier.COMMON, + [MoveId.AIR_CUTTER]: RewardTier.GREAT, + [MoveId.OVERHEAT]: RewardTier.ULTRA, + [MoveId.ROCK_TOMB]: RewardTier.GREAT, + [MoveId.METAL_SOUND]: RewardTier.COMMON, + [MoveId.COSMIC_POWER]: RewardTier.COMMON, + [MoveId.SIGNAL_BEAM]: RewardTier.GREAT, + [MoveId.SAND_TOMB]: RewardTier.COMMON, + [MoveId.MUDDY_WATER]: RewardTier.GREAT, + [MoveId.BULLET_SEED]: RewardTier.GREAT, + [MoveId.AERIAL_ACE]: RewardTier.GREAT, + [MoveId.ICICLE_SPEAR]: RewardTier.GREAT, + [MoveId.IRON_DEFENSE]: RewardTier.GREAT, + [MoveId.DRAGON_CLAW]: RewardTier.ULTRA, + [MoveId.FRENZY_PLANT]: RewardTier.ULTRA, + [MoveId.BULK_UP]: RewardTier.COMMON, + [MoveId.BOUNCE]: RewardTier.GREAT, + [MoveId.MUD_SHOT]: RewardTier.GREAT, + [MoveId.POISON_TAIL]: RewardTier.GREAT, + [MoveId.COVET]: RewardTier.GREAT, + [MoveId.MAGICAL_LEAF]: RewardTier.GREAT, + [MoveId.CALM_MIND]: RewardTier.GREAT, + [MoveId.LEAF_BLADE]: RewardTier.ULTRA, + [MoveId.DRAGON_DANCE]: RewardTier.GREAT, + [MoveId.ROCK_BLAST]: RewardTier.GREAT, + [MoveId.WATER_PULSE]: RewardTier.GREAT, + [MoveId.ROOST]: RewardTier.GREAT, + [MoveId.GRAVITY]: RewardTier.COMMON, + [MoveId.GYRO_BALL]: RewardTier.COMMON, + [MoveId.BRINE]: RewardTier.GREAT, + [MoveId.PLUCK]: RewardTier.GREAT, + [MoveId.TAILWIND]: RewardTier.GREAT, + [MoveId.U_TURN]: RewardTier.GREAT, + [MoveId.CLOSE_COMBAT]: RewardTier.ULTRA, + [MoveId.PAYBACK]: RewardTier.COMMON, + [MoveId.ASSURANCE]: RewardTier.COMMON, + [MoveId.EMBARGO]: RewardTier.COMMON, + [MoveId.FLING]: RewardTier.COMMON, + [MoveId.GASTRO_ACID]: RewardTier.GREAT, + [MoveId.POWER_SWAP]: RewardTier.COMMON, + [MoveId.GUARD_SWAP]: RewardTier.COMMON, + [MoveId.WORRY_SEED]: RewardTier.GREAT, + [MoveId.TOXIC_SPIKES]: RewardTier.GREAT, + [MoveId.FLARE_BLITZ]: RewardTier.ULTRA, + [MoveId.AURA_SPHERE]: RewardTier.GREAT, + [MoveId.ROCK_POLISH]: RewardTier.COMMON, + [MoveId.POISON_JAB]: RewardTier.GREAT, + [MoveId.DARK_PULSE]: RewardTier.GREAT, + [MoveId.AQUA_TAIL]: RewardTier.GREAT, + [MoveId.SEED_BOMB]: RewardTier.GREAT, + [MoveId.AIR_SLASH]: RewardTier.GREAT, + [MoveId.X_SCISSOR]: RewardTier.GREAT, + [MoveId.BUG_BUZZ]: RewardTier.GREAT, + [MoveId.DRAGON_PULSE]: RewardTier.GREAT, + [MoveId.POWER_GEM]: RewardTier.GREAT, + [MoveId.DRAIN_PUNCH]: RewardTier.GREAT, + [MoveId.VACUUM_WAVE]: RewardTier.COMMON, + [MoveId.FOCUS_BLAST]: RewardTier.GREAT, + [MoveId.ENERGY_BALL]: RewardTier.GREAT, + [MoveId.BRAVE_BIRD]: RewardTier.ULTRA, + [MoveId.EARTH_POWER]: RewardTier.ULTRA, + [MoveId.GIGA_IMPACT]: RewardTier.GREAT, + [MoveId.NASTY_PLOT]: RewardTier.COMMON, + [MoveId.AVALANCHE]: RewardTier.GREAT, + [MoveId.SHADOW_CLAW]: RewardTier.GREAT, + [MoveId.THUNDER_FANG]: RewardTier.GREAT, + [MoveId.ICE_FANG]: RewardTier.GREAT, + [MoveId.FIRE_FANG]: RewardTier.GREAT, + [MoveId.PSYCHO_CUT]: RewardTier.GREAT, + [MoveId.ZEN_HEADBUTT]: RewardTier.GREAT, + [MoveId.FLASH_CANNON]: RewardTier.GREAT, + [MoveId.ROCK_CLIMB]: RewardTier.GREAT, + [MoveId.DEFOG]: RewardTier.COMMON, + [MoveId.TRICK_ROOM]: RewardTier.COMMON, + [MoveId.DRACO_METEOR]: RewardTier.ULTRA, + [MoveId.LEAF_STORM]: RewardTier.ULTRA, + [MoveId.POWER_WHIP]: RewardTier.ULTRA, + [MoveId.CROSS_POISON]: RewardTier.GREAT, + [MoveId.GUNK_SHOT]: RewardTier.ULTRA, + [MoveId.IRON_HEAD]: RewardTier.GREAT, + [MoveId.STONE_EDGE]: RewardTier.ULTRA, + [MoveId.STEALTH_ROCK]: RewardTier.COMMON, + [MoveId.GRASS_KNOT]: RewardTier.ULTRA, + [MoveId.BUG_BITE]: RewardTier.GREAT, + [MoveId.CHARGE_BEAM]: RewardTier.GREAT, + [MoveId.HONE_CLAWS]: RewardTier.COMMON, + [MoveId.WONDER_ROOM]: RewardTier.COMMON, + [MoveId.PSYSHOCK]: RewardTier.GREAT, + [MoveId.VENOSHOCK]: RewardTier.GREAT, + [MoveId.MAGIC_ROOM]: RewardTier.COMMON, + [MoveId.SMACK_DOWN]: RewardTier.COMMON, + [MoveId.SLUDGE_WAVE]: RewardTier.GREAT, + [MoveId.HEAVY_SLAM]: RewardTier.GREAT, + [MoveId.ELECTRO_BALL]: RewardTier.GREAT, + [MoveId.FLAME_CHARGE]: RewardTier.GREAT, + [MoveId.LOW_SWEEP]: RewardTier.GREAT, + [MoveId.ACID_SPRAY]: RewardTier.COMMON, + [MoveId.FOUL_PLAY]: RewardTier.ULTRA, + [MoveId.ROUND]: RewardTier.COMMON, + [MoveId.ECHOED_VOICE]: RewardTier.COMMON, + [MoveId.STORED_POWER]: RewardTier.COMMON, + [MoveId.ALLY_SWITCH]: RewardTier.COMMON, + [MoveId.SCALD]: RewardTier.GREAT, + [MoveId.HEX]: RewardTier.GREAT, + [MoveId.SKY_DROP]: RewardTier.GREAT, + [MoveId.INCINERATE]: RewardTier.GREAT, + [MoveId.QUASH]: RewardTier.COMMON, + [MoveId.ACROBATICS]: RewardTier.GREAT, + [MoveId.RETALIATE]: RewardTier.GREAT, + [MoveId.WATER_PLEDGE]: RewardTier.GREAT, + [MoveId.FIRE_PLEDGE]: RewardTier.GREAT, + [MoveId.GRASS_PLEDGE]: RewardTier.GREAT, + [MoveId.VOLT_SWITCH]: RewardTier.GREAT, + [MoveId.STRUGGLE_BUG]: RewardTier.COMMON, + [MoveId.BULLDOZE]: RewardTier.GREAT, + [MoveId.FROST_BREATH]: RewardTier.GREAT, + [MoveId.DRAGON_TAIL]: RewardTier.GREAT, + [MoveId.WORK_UP]: RewardTier.COMMON, + [MoveId.ELECTROWEB]: RewardTier.GREAT, + [MoveId.WILD_CHARGE]: RewardTier.GREAT, + [MoveId.DRILL_RUN]: RewardTier.GREAT, + [MoveId.RAZOR_SHELL]: RewardTier.GREAT, + [MoveId.HEAT_CRASH]: RewardTier.GREAT, + [MoveId.TAIL_SLAP]: RewardTier.GREAT, + [MoveId.HURRICANE]: RewardTier.ULTRA, + [MoveId.SNARL]: RewardTier.COMMON, + [MoveId.PHANTOM_FORCE]: RewardTier.ULTRA, + [MoveId.PETAL_BLIZZARD]: RewardTier.GREAT, + [MoveId.DISARMING_VOICE]: RewardTier.GREAT, + [MoveId.DRAINING_KISS]: RewardTier.GREAT, + [MoveId.GRASSY_TERRAIN]: RewardTier.COMMON, + [MoveId.MISTY_TERRAIN]: RewardTier.COMMON, + [MoveId.PLAY_ROUGH]: RewardTier.GREAT, + [MoveId.CONFIDE]: RewardTier.COMMON, + [MoveId.MYSTICAL_FIRE]: RewardTier.GREAT, + [MoveId.EERIE_IMPULSE]: RewardTier.COMMON, + [MoveId.VENOM_DRENCH]: RewardTier.COMMON, + [MoveId.ELECTRIC_TERRAIN]: RewardTier.COMMON, + [MoveId.DAZZLING_GLEAM]: RewardTier.ULTRA, + [MoveId.INFESTATION]: RewardTier.COMMON, + [MoveId.POWER_UP_PUNCH]: RewardTier.GREAT, + [MoveId.DARKEST_LARIAT]: RewardTier.GREAT, + [MoveId.HIGH_HORSEPOWER]: RewardTier.ULTRA, + [MoveId.SOLAR_BLADE]: RewardTier.GREAT, + [MoveId.THROAT_CHOP]: RewardTier.GREAT, + [MoveId.POLLEN_PUFF]: RewardTier.GREAT, + [MoveId.PSYCHIC_TERRAIN]: RewardTier.COMMON, + [MoveId.LUNGE]: RewardTier.GREAT, + [MoveId.SPEED_SWAP]: RewardTier.COMMON, + [MoveId.SMART_STRIKE]: RewardTier.GREAT, + [MoveId.BRUTAL_SWING]: RewardTier.GREAT, + [MoveId.AURORA_VEIL]: RewardTier.COMMON, + [MoveId.PSYCHIC_FANGS]: RewardTier.GREAT, + [MoveId.STOMPING_TANTRUM]: RewardTier.GREAT, + [MoveId.LIQUIDATION]: RewardTier.ULTRA, + [MoveId.BODY_PRESS]: RewardTier.ULTRA, + [MoveId.BREAKING_SWIPE]: RewardTier.GREAT, + [MoveId.STEEL_BEAM]: RewardTier.ULTRA, + [MoveId.EXPANDING_FORCE]: RewardTier.GREAT, + [MoveId.STEEL_ROLLER]: RewardTier.COMMON, + [MoveId.SCALE_SHOT]: RewardTier.ULTRA, + [MoveId.METEOR_BEAM]: RewardTier.GREAT, + [MoveId.MISTY_EXPLOSION]: RewardTier.COMMON, + [MoveId.GRASSY_GLIDE]: RewardTier.COMMON, + [MoveId.RISING_VOLTAGE]: RewardTier.COMMON, + [MoveId.TERRAIN_PULSE]: RewardTier.COMMON, + [MoveId.SKITTER_SMACK]: RewardTier.GREAT, + [MoveId.BURNING_JEALOUSY]: RewardTier.GREAT, + [MoveId.LASH_OUT]: RewardTier.GREAT, + [MoveId.POLTERGEIST]: RewardTier.ULTRA, + [MoveId.CORROSIVE_GAS]: RewardTier.COMMON, + [MoveId.COACHING]: RewardTier.COMMON, + [MoveId.FLIP_TURN]: RewardTier.COMMON, + [MoveId.TRIPLE_AXEL]: RewardTier.COMMON, + [MoveId.DUAL_WINGBEAT]: RewardTier.COMMON, + [MoveId.SCORCHING_SANDS]: RewardTier.GREAT, + [MoveId.TERA_BLAST]: RewardTier.GREAT, + [MoveId.ICE_SPINNER]: RewardTier.GREAT, + [MoveId.SNOWSCAPE]: RewardTier.COMMON, + [MoveId.POUNCE]: RewardTier.COMMON, + [MoveId.TRAILBLAZE]: RewardTier.COMMON, + [MoveId.CHILLING_WATER]: RewardTier.COMMON, + [MoveId.HARD_PRESS]: RewardTier.GREAT, + [MoveId.DRAGON_CHEER]: RewardTier.COMMON, + [MoveId.ALLURING_VOICE]: RewardTier.GREAT, + [MoveId.TEMPER_FLARE]: RewardTier.GREAT, + [MoveId.SUPERCELL_SLAM]: RewardTier.GREAT, + [MoveId.PSYCHIC_NOISE]: RewardTier.GREAT, + [MoveId.UPPER_HAND]: RewardTier.COMMON, }; diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 3fdd83c185d..09b00f1af2c 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -22,7 +22,7 @@ import { TrainerType } from "#enums/trainer-type"; import { Nature } from "#enums/nature"; import type { MoveId } from "#enums/move-id"; import { TypeColor, TypeShadow } from "#enums/color"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { globalScene } from "#app/global-scene"; import { pokemonFormChanges } from "./pokemon-forms"; import { pokemonEvolutions } from "./balance/pokemon-evolutions"; @@ -459,11 +459,11 @@ export class SingleGenerationChallenge extends Challenge { .setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true)) .setCustomModifierRewards({ guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, + RewardTier.ROGUE, + RewardTier.ROGUE, + RewardTier.ULTRA, + RewardTier.ULTRA, + RewardTier.ULTRA, ], allowLuckUpgrades: false, }); @@ -476,12 +476,12 @@ export class SingleGenerationChallenge extends Challenge { .setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true)) .setCustomModifierRewards({ guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, + RewardTier.ROGUE, + RewardTier.ROGUE, + RewardTier.ULTRA, + RewardTier.ULTRA, + RewardTier.ULTRA, + RewardTier.ULTRA, ], allowLuckUpgrades: false, }); diff --git a/src/data/data-lists.ts b/src/data/data-lists.ts index ed172846fe1..3cae3e25ffd 100644 --- a/src/data/data-lists.ts +++ b/src/data/data-lists.ts @@ -1,5 +1,9 @@ import type PokemonSpecies from "#app/data/pokemon-species"; +import type { HeldItem } from "#app/items/held-item"; +import type { TrainerItem } from "#app/items/trainer-item"; import type { ModifierTypes } from "#app/modifier/modifier-type"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { TrainerItemId } from "#enums/trainer-item-id"; import type { Ability } from "./abilities/ability"; import type Move from "./moves/move"; @@ -7,5 +11,8 @@ export const allAbilities: Ability[] = []; export const allMoves: Move[] = []; export const allSpecies: PokemonSpecies[] = []; +export const allHeldItems: Record = {}; +export const allTrainerItems: Record = {}; + // TODO: Figure out what this is used for and provide an appropriate tsdoc comment export const modifierTypes = {} as ModifierTypes; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 0878ece2f01..04df60f0251 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -36,15 +36,7 @@ import { ArenaTagSide } from "#enums/arena-tag-side"; import { applyAbAttrs } from "../abilities/apply-ab-attrs"; -import { allAbilities, allMoves } from "../data-lists"; -import { - AttackTypeBoosterModifier, - BerryModifier, - PokemonHeldItemModifier, - PokemonMoveAccuracyBoosterModifier, - PokemonMultiHitModifier, - PreserveBerryModifier, -} from "../../modifier/modifier"; +import { allAbilities, allHeldItems, allMoves } from "../data-lists"; import type { BattlerIndex } from "#enums/battler-index"; import { BattleType } from "#enums/battle-type"; import { TerrainType } from "../terrain"; @@ -86,10 +78,15 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger"; import { MultiHitType } from "#enums/MultiHitType"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves"; import { isVirtual, MoveUseMode } from "#enums/move-use-mode"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { BerryHeldItem, berryTypeToHeldItem } from "#app/items/held-items/berry"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import { applyMoveAttrs } from "./apply-attrs"; import { frenzyMissFunc, getMoveTargets } from "./move-utils"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability"; +import { applyHeldItems } from "#app/items/all-held-items"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -769,7 +766,7 @@ export default abstract class Move implements Localizable { const isOhko = this.hasAttr("OneHitKOAccuracyAttr"); if (!isOhko) { - globalScene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); + applyHeldItems(HELD_ITEM_EFFECT.ACCURACY_BOOSTER, { pokemon: user, moveAccuracy: moveAccuracy }); } if (globalScene.arena.weather?.weatherType === WeatherType.FOG) { @@ -806,7 +803,7 @@ export default abstract class Move implements Localizable { applyAbAttrs("MoveTypeChangeAbAttr", {pokemon: source, opponent: target, move: this, simulated: true, moveType: typeChangeHolder, power: typeChangeMovePowerMultiplier}); const sourceTeraType = source.getTeraType(); - if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) { + if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !source.heldItemManager.hasItem(HeldItemId.MULTI_LENS)) { power.value = 60; } @@ -851,7 +848,11 @@ export default abstract class Move implements Localizable { if (!this.hasAttr("TypelessAttr")) { globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power); - globalScene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, typeChangeHolder.value, power); + applyHeldItems(HELD_ITEM_EFFECT.ATTACK_TYPE_BOOST, { + pokemon: source, + moveType: typeChangeHolder.value, + movePower: power, + }); } if (source.getTag(HelpingHandTag)) { @@ -914,7 +915,7 @@ export default abstract class Move implements Localizable { * Returns `true` if this move can be given additional strikes * by enhancing effects. * Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond} - * and {@linkcode PokemonMultiHitModifier | Multi-Lens}. + * and {@linkcode MultiHitHeldItem | Multi-Lens}. * @param user The {@linkcode Pokemon} using the move * @param restrictSpread `true` if the enhancing effect * should not affect multi-target moves (default `false`) @@ -1565,7 +1566,7 @@ export class TargetHalfHpDamageAttr extends FixedDamageAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { // first, determine if the hit is coming from multi lens or not - const lensCount = user.getHeldItems().find(i => i instanceof PokemonMultiHitModifier)?.getStackCount() ?? 0; + const lensCount = user.heldItemManager.getStack(HeldItemId.MULTI_LENS); if (lensCount <= 0) { // no multi lenses; we can just halve the target's hp and call it a day (args[0] as NumberHolder).value = toDmgValue(target.hp / 2); @@ -2621,35 +2622,33 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { return false; } - const heldItems = this.getTargetHeldItems(target).filter((i) => i.isTransferable); + const heldItems = target.heldItemManager.getTransferableHeldItems(); if (!heldItems.length) { return false; } - const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD; - const highestItemTier = heldItems.map((m) => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct? - const tierHeldItems = heldItems.filter((m) => m.type.getOrInferTier(poolType) === highestItemTier); - const stolenItem = tierHeldItems[user.randBattleSeedInt(tierHeldItems.length)]; - if (!globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) { + const stolenItem = heldItems[user.randBattleSeedInt(heldItems.length)]; + + if (!globalScene.tryTransferHeldItem(stolenItem, target, user, false)) { return false; } - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name })); + globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:stoleItem", + { pokemonName: getPokemonNameWithAffix(user), + targetName: getPokemonNameWithAffix(target), + itemName: allHeldItems[stolenItem].name + } + )); return true; } - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[]; - } - getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - const heldItems = this.getTargetHeldItems(target); + const heldItems = target.heldItemManager.getTransferableHeldItems(); return heldItems.length ? 5 : 0; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - const heldItems = this.getTargetHeldItems(target); + const heldItems = target.heldItemManager.getTransferableHeldItems(); return heldItems.length ? -5 : 0; } } @@ -2695,10 +2694,10 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Considers entire transferrable item pool by default (Knock Off). // Otherwise only consider berries (Incinerate). - let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); + let heldItems = target.heldItemManager.getTransferableHeldItems(); if (this.berriesOnly) { - heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer()); + heldItems = heldItems.filter(m => m in Object.values(berryTypeToHeldItem)); } if (!heldItems.length) { @@ -2709,29 +2708,26 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Decrease item amount and update icon target.loseHeldItem(removedItem); - globalScene.updateModifiers(target.isPlayer()); + globalScene.updateItems(target.isPlayer()); if (this.berriesOnly) { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); + globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:incineratedItem", + { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: allHeldItems[removedItem].name })); } else { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:knockedOffItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); + globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:knockedOffItem", + { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: allHeldItems[removedItem].name })); } return true; } - - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[]; - } - + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - const heldItems = this.getTargetHeldItems(target); + const heldItems = target.getHeldItems(); return heldItems.length ? 5 : 0; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - const heldItems = this.getTargetHeldItems(target); + const heldItems = target.getHeldItems(); return heldItems.length ? -5 : 0; } } @@ -2740,7 +2736,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { * Attribute that causes targets of the move to eat a berry. Used for Teatime, Stuff Cheeks */ export class EatBerryAttr extends MoveEffectAttr { - protected chosenBerry: BerryModifier; + protected chosenBerry: HeldItemId; constructor(selfTarget: boolean) { super(selfTarget); } @@ -2769,7 +2765,7 @@ export class EatBerryAttr extends MoveEffectAttr { this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; const preserve = new BooleanHolder(false); // check for berry pouch preservation - globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.PRESERVE_BERRY, {pokemon: pokemon, doPreserve: preserve}); if (!preserve.value) { this.reduceBerryModifier(pokemon); } @@ -2780,16 +2776,15 @@ export class EatBerryAttr extends MoveEffectAttr { return true; } - getTargetHeldBerries(target: Pokemon): BerryModifier[] { - return globalScene.findModifiers(m => m instanceof BerryModifier - && (m as BerryModifier).pokemonId === target.id, target.isPlayer()) as BerryModifier[]; + getTargetHeldBerries(target: Pokemon): HeldItemId[] { + return target.getHeldItems().filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY)); } reduceBerryModifier(target: Pokemon) { if (this.chosenBerry) { target.loseHeldItem(this.chosenBerry); } - globalScene.updateModifiers(target.isPlayer()); + globalScene.updateItems(target.isPlayer()); } @@ -2803,10 +2798,10 @@ export class EatBerryAttr extends MoveEffectAttr { */ protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) { // consumer eats berry, owner triggers unburden and similar effects - getBerryEffectFunc(this.chosenBerry.berryType)(consumer); + getBerryEffectFunc((allHeldItems[this.chosenBerry] as BerryHeldItem).berryType)(consumer); applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner}); applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer}); - consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest); + consumer.recordEatenBerry((allHeldItems[this.chosenBerry] as BerryHeldItem).berryType, updateHarvest); } } @@ -2845,7 +2840,7 @@ export class StealEatBerryAttr extends EatBerryAttr { // pick a random berry and eat it this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; applyAbAttrs("PostItemLostAbAttr", {pokemon: target}); - const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); + const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: allHeldItems[this.chosenBerry].name }); globalScene.phaseManager.queueMessage(message); this.reduceBerryModifier(target); this.eatBerry(user, target); @@ -6424,9 +6419,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } } - // clear out enemy held item modifiers of the switch out target - globalScene.clearEnemyHeldItemModifiers(switchOutTarget); - if (!allyPokemon?.isActive(true) && switchOutTarget.hp) { globalScene.phaseManager.pushNew("BattleEndPhase", false); @@ -8017,14 +8009,14 @@ const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Po const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => !target.isOfType(PokemonType.GHOST); -const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0; +const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.heldItemManager.getTransferableHeldItems().length > 0; const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => { - const heldItems = target.getHeldItems().filter(i => i.isTransferable); + const heldItems = target.heldItemManager.getTransferableHeldItems(); if (heldItems.length === 0) { return ""; } - const itemName = heldItems[0]?.type?.name ?? "item"; + const itemName = allHeldItems[heldItems[0]].name ?? "item"; const message: string = i18next.t("moveTriggers:attackedByItem", { pokemonName: getPokemonNameWithAffix(target), itemName: itemName }); return message; }; @@ -9319,7 +9311,7 @@ export function initMoves() { .condition((user, target, move) => !target.status && !target.isSafeguarded(user)) .reflectable(), new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) - .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) + .attr(MovePowerMultiplierAttr, (user, target, move) => target.heldItemManager.getTransferableHeldItems().length > 0 ? 1.5 : 1) .attr(RemoveHeldItemAttr, false) .edgeCase(), // Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc. @@ -10041,7 +10033,7 @@ export function initMoves() { .condition((user, target, move) => !target.turnData.acted) .attr(ForceLastAttr), new AttackMove(MoveId.ACROBATICS, PokemonType.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) - .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), + .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.heldItemManager.getTransferableHeldItems().reduce((v, m) => v + user.heldItemManager.getStack(m), 0))), new StatusMove(MoveId.REFLECT_TYPE, PokemonType.NORMAL, -1, 15, -1, 0, 5) .ignoresSubstitute() .attr(CopyTypeAttr), @@ -10790,7 +10782,7 @@ export function initMoves() { .attr(EatBerryAttr, true) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .condition((user) => { - const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()); + const userBerries = user.getHeldItems().filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY)); return userBerries.length > 0; }) .edgeCase(), // Stuff Cheeks should not be selectable when the user does not have a berry, see wiki diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index 7a1c9821e89..4c88add526a 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -19,7 +19,7 @@ import i18next from "i18next"; import type { IEggOptions } from "#app/data/egg"; import { EggSourceType } from "#enums/egg-source-types"; import { EggTier } from "#enums/egg-type"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { modifierTypes } from "#app/data/data-lists"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; @@ -165,7 +165,7 @@ export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder. setEncounterRewards( { guaranteedModifierTypeFuncs: [modifierTypes.SACRED_ASH], - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA], + guaranteedModifierTiers: [RewardTier.ROGUE, RewardTier.ULTRA], fillRemaining: true, }, [eggOptions], diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index bec75288837..e1d59b58670 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -1,6 +1,6 @@ import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { - generateModifierType, + getPartyBerries, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, @@ -9,40 +9,47 @@ import { import type Pokemon from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; -import type { BerryModifierType, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { BerryModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { MoveId } from "#enums/move-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { randInt } from "#app/utils/common"; import { BattlerIndex } from "#enums/battler-index"; -import { - applyModifierTypeToPlayerPokemon, - catchPokemon, - getHighestLevelPlayerPokemon, -} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { TrainerSlot } from "#enums/trainer-slot"; import { PokeballType } from "#enums/pokeball"; -import type HeldModifierConfig from "#app/@types/held-modifier-config"; -import type { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import i18next from "i18next"; +import type { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro"; import { MoveUseMode } from "#enums/move-use-mode"; +import type { HeldItemConfiguration } from "#app/items/held-item-data-types"; +import { allHeldItems } from "#app/data/data-lists"; +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { HeldItemRequirement } from "../mystery-encounter-requirements"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/absoluteAvarice"; +function berrySprite(spriteKey: string, x: number, y: number): MysteryEncounterSpriteConfig { + return { + spriteKey: spriteKey, + fileRoot: "items", + isItem: true, + x: x, + y: y, + hidden: true, + disableAnimation: true, + }; +} + /** * Absolute Avarice encounter. * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805} @@ -53,7 +60,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde ) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(20, 180) - .withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 6)) // Must have at least 6 berries to spawn + .withSceneRequirement(new HeldItemRequirement(HeldItemCategoryId.BERRY, 6)) // Must have at least 6 berries to spawn .withFleeAllowed(false) .withIntroSpriteConfigs([ { @@ -74,105 +81,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde repeat: true, x: -5, }, - { - spriteKey: "lum_berry", - fileRoot: "items", - isItem: true, - x: 7, - y: -14, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "salac_berry", - fileRoot: "items", - isItem: true, - x: 2, - y: 4, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "lansat_berry", - fileRoot: "items", - isItem: true, - x: 32, - y: 5, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "liechi_berry", - fileRoot: "items", - isItem: true, - x: 6, - y: -5, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "sitrus_berry", - fileRoot: "items", - isItem: true, - x: 7, - y: 8, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "enigma_berry", - fileRoot: "items", - isItem: true, - x: 26, - y: -4, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "leppa_berry", - fileRoot: "items", - isItem: true, - x: 16, - y: -27, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "petaya_berry", - fileRoot: "items", - isItem: true, - x: 30, - y: -17, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "ganlon_berry", - fileRoot: "items", - isItem: true, - x: 16, - y: -11, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "apicot_berry", - fileRoot: "items", - isItem: true, - x: 14, - y: -2, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "starf_berry", - fileRoot: "items", - isItem: true, - x: 18, - y: 9, - hidden: true, - disableAnimation: true, - }, + berrySprite("lum_berry", 7, -14), + berrySprite("salac_berry", 2, 4), + berrySprite("lansat_berry", 32, 5), + berrySprite("liechi_berry", 6, -5), + berrySprite("sitrus_berry", 7, 8), + berrySprite("enigma_berry", 26, -4), + berrySprite("leppa_berry", 16, -27), + berrySprite("petaya_berry", 30, -17), + berrySprite("ganlon_berry", 16, -11), + berrySprite("apicot_berry", 14, -2), + berrySprite("starf_berry", 18, 9), ]) .withHideWildIntroMessage(true) .withAutoHideIntroVisuals(false) @@ -191,35 +110,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde globalScene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); globalScene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); - // Get all player berry items, remove from party, and store reference - const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + // Get all berries in party, with references to the pokemon + const berryItems = getPartyBerries(); - // Sort berries by party member ID to more easily re-add later if necessary - const berryItemsMap = new Map(); - globalScene.getPlayerParty().forEach(pokemon => { - const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); - if (pokemonBerries?.length > 0) { - berryItemsMap.set(pokemon.id, pokemonBerries); - } + encounter.misc.berryItemsMap = berryItems; + + // Adds stolen berries to the Greedent item configuration + const bossHeldItemConfig: HeldItemConfiguration = []; + berryItems.forEach(map => { + bossHeldItemConfig.push({ entry: map.item, count: 1 }); }); - encounter.misc = { berryItemsMap }; - - // Generates copies of the stolen berries to put on the Greedent - const bossModifierConfigs: HeldModifierConfig[] = []; - berryItems.forEach(berryMod => { - // Can't define stack count on a ModifierType, have to just create separate instances for each stack - // Overflow berries will be "lost" on the boss, but it's un-catchable anyway - for (let i = 0; i < berryMod.stackCount; i++) { - const modifierType = generateModifierType(modifierTypes.BERRY, [ - berryMod.berryType, - ]) as PokemonHeldItemModifierType; - bossModifierConfigs.push({ modifier: modifierType }); - } - }); - - // Do NOT remove the real berries yet or else it will be persisted in the session data - // +1 SpDef below wave 50, SpDef and Speed otherwise const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] = globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.SPDEF, Stat.SPD]; @@ -234,7 +135,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde bossSegments: 3, shiny: false, // Shiny lock because of consistency issues between the different options moveSet: [MoveId.THRASH, MoveId.CRUNCH, MoveId.BODY_PRESS, MoveId.SLACK_OFF], - modifierConfigs: bossModifierConfigs, + heldItemConfig: bossHeldItemConfig, tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { queueEncounterMessage(`${namespace}:option.1.boss_enraged`); @@ -261,12 +162,12 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde // Remove the berries from the party // Session has been safely saved at this point, so data won't be lost - const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; - berryItems.forEach(berryMod => { - globalScene.removeModifier(berryMod); + const berryItems = getPartyBerries(); + berryItems.forEach(map => { + globalScene.getPokemonById(map.pokemonId)?.heldItemManager.remove(map.item.id); }); - globalScene.updateModifiers(true); + globalScene.updateItems(true); return true; }) @@ -286,19 +187,14 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde const encounter = globalScene.currentBattle.mysteryEncounter!; // Provides 1x Reviver Seed to each party member at end of battle - const revSeed = generateModifierType(modifierTypes.REVIVER_SEED); encounter.setDialogueToken( "foodReward", - revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"), + allHeldItems[HeldItemId.REVIVER_SEED].name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"), ); const givePartyPokemonReviverSeeds = () => { const party = globalScene.getPlayerParty(); party.forEach(p => { - const heldItems = p.getHeldItems(); - if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) { - const seedModifier = revSeed.newModifier(p); - globalScene.addModifier(seedModifier, false, false, false, true); - } + p.heldItemManager.add(HeldItemId.REVIVER_SEED); }); queueEncounterMessage(`${namespace}:option.1.food_stash`); }; @@ -334,23 +230,20 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde // Returns 2/5 of the berries stolen to each Pokemon const party = globalScene.getPlayerParty(); party.forEach(pokemon => { - const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); - const berryTypesAsArray: BerryType[] = []; - stolenBerries?.forEach(bMod => berryTypesAsArray.push(...new Array(bMod.stackCount).fill(bMod.berryType))); - const returnedBerryCount = Math.floor(((berryTypesAsArray.length ?? 0) * 2) / 5); + // TODO: is this check legal? + const stolenBerries = berryMap.filter(map => map.pokemon === pokemon); + const returnedBerryCount = Math.floor(((stolenBerries.length ?? 0) * 2) / 5); if (returnedBerryCount > 0) { for (let i = 0; i < returnedBerryCount; i++) { // Shuffle remaining berry types and pop - Phaser.Math.RND.shuffle(berryTypesAsArray); - const randBerryType = berryTypesAsArray.pop(); - - const berryModType = generateModifierType(modifierTypes.BERRY, [randBerryType]) as BerryModifierType; - applyModifierTypeToPlayerPokemon(pokemon, berryModType); + Phaser.Math.RND.shuffle(stolenBerries); + const randBerryType = stolenBerries.pop(); + pokemon.heldItemManager.add(randBerryType); } } }); - await globalScene.updateModifiers(true); + await globalScene.updateItems(true); await transitionMysteryEncounterIntroVisuals(true, true, 500); leaveEncounterWithoutBattle(true); diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index f8a904cdb45..37ddeaea3d2 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -1,5 +1,4 @@ import { - generateModifierType, leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, @@ -24,6 +23,8 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import i18next from "i18next"; +import { allTrainerItems } from "#app/data/data-lists"; +import { TrainerItemId } from "#enums/trainer-item-id"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/anOfferYouCantRefuse"; @@ -109,8 +110,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB } } - const shinyCharm = generateModifierType(modifierTypes.SHINY_CHARM); - encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name")); + const name = allTrainerItems[TrainerItemId.SHINY_CHARM].name; + encounter.setDialogueToken("itemName", name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name")); encounter.setDialogueToken("liepardName", getPokemonSpecies(SpeciesId.LIEPARD).getName()); return true; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index d86a8439804..48bd27121d1 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -1,7 +1,6 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { - generateModifierType, generateModifierTypeOption, getRandomEncounterSpecies, initBattleWithEnemyConfig, @@ -11,7 +10,7 @@ import { } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import type { BerryModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; +import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import { ModifierPoolType } from "#enums/modifier-pool-type"; @@ -26,18 +25,17 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { - applyModifierTypeToPlayerPokemon, getEncounterPokemonLevelForWave, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER, } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; -import { BerryModifier } from "#app/modifier/modifier"; import i18next from "#app/plugins/i18n"; import { BerryType } from "#enums/berry-type"; import { PERMANENT_STATS, Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { berryTypeToHeldItem } from "#app/items/held-items/berry"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/berriesAbound"; @@ -312,35 +310,17 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. function tryGiveBerry(prioritizedPokemon?: PlayerPokemon) { const berryType = randSeedInt(Object.keys(BerryType).filter(s => !Number.isNaN(Number(s))).length) as BerryType; - const berry = generateModifierType(modifierTypes.BERRY, [berryType]) as BerryModifierType; + const berry = berryTypeToHeldItem[berryType]; const party = globalScene.getPlayerParty(); - // Will try to apply to prioritized pokemon first, then do normal application method if it fails - if (prioritizedPokemon) { - const heldBerriesOfType = globalScene.findModifier( - m => - m instanceof BerryModifier && - m.pokemonId === prioritizedPokemon.id && - (m as BerryModifier).berryType === berryType, - true, - ) as BerryModifier; - - if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount()) { - applyModifierTypeToPlayerPokemon(prioritizedPokemon, berry); - return; - } + // Will give the berry to a Pokemon, starting from the prioritized one + if (prioritizedPokemon?.heldItemManager.add(berry)) { + return; } - // Iterate over the party until berry was successfully given for (const pokemon of party) { - const heldBerriesOfType = globalScene.findModifier( - m => m instanceof BerryModifier && m.pokemonId === pokemon.id && (m as BerryModifier).berryType === berryType, - true, - ) as BerryModifier; - - if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount()) { - applyModifierTypeToPlayerPokemon(pokemon, berry); + if (pokemon.heldItemManager.add(berry)) { return; } } diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index df06f40c159..9bc3e293cbe 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -1,6 +1,5 @@ import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { - generateModifierType, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, @@ -31,28 +30,22 @@ import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler" import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { - AttackTypeBoosterHeldItemTypeRequirement, CombinationPokemonRequirement, HeldItemRequirement, TypeRequirement, } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { PokemonType } from "#enums/pokemon-type"; -import type { AttackTypeBoosterModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; +import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { - AttackTypeBoosterModifier, - BypassSpeedChanceModifier, - ContactHeldItemTransferChanceModifier, - GigantamaxAccessModifier, - MegaEvolutionAccessModifier, -} from "#app/modifier/modifier"; import i18next from "i18next"; import MoveInfoOverlay from "#app/ui/move-info-overlay"; import { allMoves } from "#app/data/data-lists"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { HeldItemId } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; +import { TrainerItemId } from "#enums/trainer-item-id"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/bugTypeSuperfan"; @@ -143,6 +136,8 @@ const POOL_3_POKEMON: { species: SpeciesId; formIndex?: number }[] = [ const POOL_4_POKEMON = [SpeciesId.GENESECT, SpeciesId.SLITHER_WING, SpeciesId.BUZZWOLE, SpeciesId.PHEROMOSA]; +const REQUIRED_ITEMS = [HeldItemId.QUICK_CLAW, HeldItemId.GRIP_CLAW, HeldItemId.SILVER_POWDER]; + const PHYSICAL_TUTOR_MOVES = [ MoveId.MEGAHORN, MoveId.ATTACK_ORDER, @@ -186,8 +181,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde .withPrimaryPokemonRequirement( CombinationPokemonRequirement.Some( // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team - new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), - new AttackTypeBoosterHeldItemTypeRequirement(PokemonType.BUG, 1), + new HeldItemRequirement(REQUIRED_ITEMS, 1), new TypeRequirement(PokemonType.BUG, false, 1), ), ) @@ -259,13 +253,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde }, ]; - const requiredItems = [ - generateModifierType(modifierTypes.QUICK_CLAW), - generateModifierType(modifierTypes.GRIP_CLAW), - generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [PokemonType.BUG]), - ]; - - const requiredItemString = requiredItems.map(m => m?.name ?? "unknown").join("/"); + const requiredItemString = REQUIRED_ITEMS.map(m => allHeldItems[m].name ?? "unknown").join("/"); encounter.setDialogueToken("requiredBugItems", requiredItemString); return true; @@ -366,10 +354,10 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde const modifierOptions: ModifierTypeOption[] = [generateModifierTypeOption(modifierTypes.MASTER_BALL)!]; const specialOptions: ModifierTypeOption[] = []; - if (!globalScene.findModifier(m => m instanceof MegaEvolutionAccessModifier)) { + if (!globalScene.trainerItems.hasItem(TrainerItemId.MEGA_BRACELET)) { modifierOptions.push(generateModifierTypeOption(modifierTypes.MEGA_BRACELET)!); } - if (!globalScene.findModifier(m => m instanceof GigantamaxAccessModifier)) { + if (!globalScene.trainerItems.hasItem(TrainerItemId.DYNAMAX_BAND)) { modifierOptions.push(generateModifierTypeOption(modifierTypes.DYNAMAX_BAND)!); } const nonRareEvolutionModifier = generateModifierTypeOption(modifierTypes.EVOLUTION_ITEM); @@ -415,8 +403,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde .withPrimaryPokemonRequirement( CombinationPokemonRequirement.Some( // Meets one or both of the below reqs - new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), - new AttackTypeBoosterHeldItemTypeRequirement(PokemonType.BUG, 1), + new HeldItemRequirement(REQUIRED_ITEMS, 1), ), ) .withDialogue({ @@ -439,25 +426,19 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones - const validItems = pokemon.getHeldItems().filter(item => { - return ( - (item instanceof BypassSpeedChanceModifier || - item instanceof ContactHeldItemTransferChanceModifier || - (item instanceof AttackTypeBoosterModifier && - (item.type as AttackTypeBoosterModifierType).moveType === PokemonType.BUG)) && - item.isTransferable - ); + const validItems = pokemon.heldItemManager.getTransferableHeldItems().filter(item => { + item in REQUIRED_ITEMS; }); - return validItems.map((modifier: PokemonHeldItemModifier) => { + return validItems.map((item: HeldItemId) => { const option: OptionSelectItem = { - label: modifier.type.name, + label: allHeldItems[item].name, handler: () => { // Pokemon and item selected - encounter.setDialogueToken("selectedItem", modifier.type.name); + encounter.setDialogueToken("selectedItem", allHeldItems[item].name); encounter.misc = { chosenPokemon: pokemon, - chosenModifier: modifier, + chosenItem: item, }; return true; }, @@ -469,12 +450,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde const selectableFilter = (pokemon: Pokemon) => { // If pokemon has valid item, it can be selected const hasValidItem = pokemon.getHeldItems().some(item => { - return ( - item instanceof BypassSpeedChanceModifier || - item instanceof ContactHeldItemTransferChanceModifier || - (item instanceof AttackTypeBoosterModifier && - (item.type as AttackTypeBoosterModifierType).moveType === PokemonType.BUG) - ); + item in REQUIRED_ITEMS; }); if (!hasValidItem) { return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; @@ -491,10 +467,10 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; chosenPokemon.loseHeldItem(modifier, false); - globalScene.updateModifiers(true, true); + globalScene.updateItems(true); const bugNet = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; - bugNet.type.tier = ModifierTier.ROGUE; + bugNet.type.tier = RewardTier.ROGUE; setEncounterRewards({ guaranteedModifierTypeOptions: [bugNet], diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 553c4deb74e..8652ec2b022 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -1,6 +1,5 @@ import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, @@ -11,9 +10,7 @@ import { import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; -import { ModifierTier } from "#enums/modifier-tier"; -import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; +import { RewardTier } from "#enums/reward-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { globalScene } from "#app/global-scene"; @@ -24,10 +21,7 @@ import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { AbilityId } from "#enums/ability-id"; -import { - applyAbilityOverrideToPokemon, - applyModifierTypeToPlayerPokemon, -} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyAbilityOverrideToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { PokemonType } from "#enums/pokemon-type"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -38,8 +32,6 @@ import i18next from "i18next"; import type { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; import type { PlayerPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; -import { BerryModifier } from "#app/modifier/modifier"; -import { BerryType } from "#enums/berry-type"; import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { EncounterBattleAnim } from "#app/data/battle-anims"; @@ -49,7 +41,10 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { EncounterAnim } from "#enums/encounter-anims"; import { Challenges } from "#enums/challenges"; import { MoveUseMode } from "#enums/move-use-mode"; -import { allAbilities, modifierTypes } from "#app/data/data-lists"; +import { allAbilities } from "#app/data/data-lists"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; +import { getHeldItemTier } from "#app/items/held-item-tiers"; +import { assignItemsFromConfiguration } from "#app/items/held-item-pool"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/clowningAround"; @@ -283,16 +278,16 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder const party = globalScene.getPlayerParty(); let mostHeldItemsPokemon = party[0]; - let count = mostHeldItemsPokemon - .getHeldItems() - .filter(m => m.isTransferable && !(m instanceof BerryModifier)) - .reduce((v, m) => v + m.stackCount, 0); + let count = mostHeldItemsPokemon.heldItemManager + .getTransferableHeldItems() + .filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY)) + .reduce((v, m) => v + mostHeldItemsPokemon.heldItemManager.getStack(m), 0); for (const pokemon of party) { - const nextCount = pokemon - .getHeldItems() - .filter(m => m.isTransferable && !(m instanceof BerryModifier)) - .reduce((v, m) => v + m.stackCount, 0); + const nextCount = pokemon.heldItemManager + .getTransferableHeldItems() + .filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY)) + .reduce((v, m) => v + pokemon.heldItemManager.getStack(m), 0); if (nextCount > count) { mostHeldItemsPokemon = pokemon; count = nextCount; @@ -301,16 +296,31 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender()); - const items = mostHeldItemsPokemon.getHeldItems(); + const items = mostHeldItemsPokemon.heldItemManager + .getTransferableHeldItems() + .filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY)); // Shuffles Berries (if they have any) + const oldBerries = mostHeldItemsPokemon.heldItemManager + .getHeldItems() + .filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY)); + let numBerries = 0; - for (const m of items.filter(m => m instanceof BerryModifier)) { - numBerries += m.stackCount; - globalScene.removeModifier(m); + for (const berry of oldBerries) { + const stack = mostHeldItemsPokemon.heldItemManager.getStack(berry); + numBerries += stack; + mostHeldItemsPokemon.heldItemManager.remove(berry, stack); } - generateItemsOfTier(mostHeldItemsPokemon, numBerries, "Berries"); + assignItemsFromConfiguration( + [ + { + entry: HeldItemCategoryId.BERRY, + count: numBerries, + }, + ], + mostHeldItemsPokemon, + ); // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) // For the purpose of this ME, Soothe Bells and Lucky Eggs are counted as Ultra tier @@ -318,20 +328,36 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder let numUltra = 0; let numRogue = 0; - for (const m of items.filter(m => m.isTransferable && !(m instanceof BerryModifier))) { - const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party); - const tier = type.tier ?? ModifierTier.ULTRA; - if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { - numRogue += m.stackCount; - globalScene.removeModifier(m); - } else if (type.id === "LUCKY_EGG" || type.id === "SOOTHE_BELL" || tier === ModifierTier.ULTRA) { - numUltra += m.stackCount; - globalScene.removeModifier(m); + for (const m of items) { + const tier = getHeldItemTier(m) ?? RewardTier.ULTRA; + const stack = mostHeldItemsPokemon.heldItemManager.getStack(m); + if (tier === RewardTier.ROGUE) { + numRogue += stack; + } else if (tier === RewardTier.ULTRA) { + numUltra += stack; } + mostHeldItemsPokemon.heldItemManager.remove(m, stack); } - generateItemsOfTier(mostHeldItemsPokemon, numUltra, ModifierTier.ULTRA); - generateItemsOfTier(mostHeldItemsPokemon, numRogue, ModifierTier.ROGUE); + assignItemsFromConfiguration( + [ + { + entry: ultraPool, + count: numUltra, + }, + ], + mostHeldItemsPokemon, + ); + + assignItemsFromConfiguration( + [ + { + entry: roguePool, + count: numRogue, + }, + ], + mostHeldItemsPokemon, + ); }) .withOptionPhase(async () => { leaveEncounterWithoutBattle(true); @@ -487,68 +513,21 @@ function onYesAbilitySwap(resolve) { selectPokemonForOption(onPokemonSelected, onPokemonNotSelected); } -function generateItemsOfTier(pokemon: PlayerPokemon, numItems: number, tier: ModifierTier | "Berries") { - // These pools have to be defined at runtime so that modifierTypes exist - // Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon - // This is to prevent "over-generating" a random item of a certain type during item swaps - const ultraPool = [ - [modifierTypes.REVIVER_SEED, 1], - [modifierTypes.GOLDEN_PUNCH, 5], - [modifierTypes.ATTACK_TYPE_BOOSTER, 99], - [modifierTypes.QUICK_CLAW, 3], - [modifierTypes.WIDE_LENS, 3], - ]; +const ultraPool = [ + { entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 1 }, + { entry: HeldItemId.REVIVER_SEED, weight: 1 }, + { entry: HeldItemId.GOLDEN_PUNCH, weight: 1 }, + { entry: HeldItemId.QUICK_CLAW, weight: 1 }, + { entry: HeldItemId.WIDE_LENS, weight: 1 }, +]; - const roguePool = [ - [modifierTypes.LEFTOVERS, 4], - [modifierTypes.SHELL_BELL, 4], - [modifierTypes.SOUL_DEW, 10], - [modifierTypes.SCOPE_LENS, 1], - [modifierTypes.BATON, 1], - [modifierTypes.FOCUS_BAND, 5], - [modifierTypes.KINGS_ROCK, 3], - [modifierTypes.GRIP_CLAW, 5], - ]; - - const berryPool = [ - [BerryType.APICOT, 3], - [BerryType.ENIGMA, 2], - [BerryType.GANLON, 3], - [BerryType.LANSAT, 3], - [BerryType.LEPPA, 2], - [BerryType.LIECHI, 3], - [BerryType.LUM, 2], - [BerryType.PETAYA, 3], - [BerryType.SALAC, 2], - [BerryType.SITRUS, 2], - [BerryType.STARF, 3], - ]; - - let pool: any[]; - if (tier === "Berries") { - pool = berryPool; - } else { - pool = tier === ModifierTier.ULTRA ? ultraPool : roguePool; - } - - for (let i = 0; i < numItems; i++) { - if (pool.length === 0) { - // Stop generating new items if somehow runs out of items to spawn - return; - } - const randIndex = randSeedInt(pool.length); - const newItemType = pool[randIndex]; - let newMod: PokemonHeldItemModifierType; - if (tier === "Berries") { - newMod = generateModifierType(modifierTypes.BERRY, [newItemType[0]]) as PokemonHeldItemModifierType; - } else { - newMod = generateModifierType(newItemType[0]) as PokemonHeldItemModifierType; - } - applyModifierTypeToPlayerPokemon(pokemon, newMod); - // Decrement max stacks and remove from pool if at max - newItemType[1]--; - if (newItemType[1] <= 0) { - pool.splice(randIndex, 1); - } - } -} +const roguePool = [ + { entry: HeldItemId.LEFTOVERS, weight: 1 }, + { entry: HeldItemId.SHELL_BELL, weight: 1 }, + { entry: HeldItemId.SOUL_DEW, weight: 1 }, + { entry: HeldItemId.SCOPE_LENS, weight: 1 }, + { entry: HeldItemId.BATON, weight: 1 }, + { entry: HeldItemId.FOCUS_BAND, weight: 1 }, + { entry: HeldItemId.KINGS_ROCK, weight: 1 }, + { entry: HeldItemId.GRIP_CLAW, weight: 1 }, +]; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 4056ba3532e..236b4fad9d7 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -16,10 +16,9 @@ import { } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { PokemonFormChangeItemModifier } from "#app/modifier/modifier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { Challenges } from "#enums/challenges"; +import type { HeldItemConfiguration } from "#app/items/held-item-data-types"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/darkDeal"; @@ -149,7 +148,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE const removedPokemon = getRandomPlayerPokemon(true, false, true); // Get all the pokemon's held items - const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); + const itemConfig = removedPokemon.heldItemManager.generateHeldItemConfiguration(); globalScene.removePokemonFromPlayerParty(removedPokemon); const encounter = globalScene.currentBattle.mysteryEncounter!; @@ -158,7 +157,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE // Store removed pokemon types encounter.misc = { removedTypes: removedPokemon.getTypes(), - modifiers, + itemConfig: itemConfig, }; }) .withOptionPhase(async () => { @@ -176,7 +175,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE bossTypes = singleTypeChallenges.map(c => (c.value - 1) as PokemonType); } - const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers; + const bossItemConfig: HeldItemConfiguration = encounter.misc.itemConfig; // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ const roll = randSeedInt(100); const starterTier: number | [number, number] = roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [9, 10]; @@ -184,12 +183,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE const pokemonConfig: EnemyPokemonConfig = { species: bossSpecies, isBoss: true, - modifierConfigs: bossModifiers.map(m => { - return { - modifier: m, - stackCount: m.getStackCount(), - }; - }), + heldItemConfig: bossItemConfig, }; if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) { pokemonConfig.formIndex = 0; diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 842fc9d73bd..a516e3265bc 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -9,25 +9,14 @@ import { } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { - generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; -import type { PokemonHeldItemModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier"; -import { - BerryModifier, - HealingBoosterModifier, - LevelIncrementBoosterModifier, - MoneyMultiplierModifier, - PreserveBerryModifier, -} from "#app/modifier/modifier"; -import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; @@ -37,31 +26,41 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { timedEventManager } from "#app/global-event-manager"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; +import { TrainerItemId } from "#enums/trainer-item-id"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/delibirdy"; /** Berries only */ -const OPTION_2_ALLOWED_MODIFIERS = ["BerryModifier", "PokemonInstantReviveModifier"]; +const OPTION_2_ALLOWED_MODIFIERS = [HeldItemCategoryId.BERRY, HeldItemId.REVIVER_SEED]; /** Disallowed items are berries, Reviver Seeds, and Vitamins (form change items and fusion items are not PokemonHeldItemModifiers) */ -const OPTION_3_DISALLOWED_MODIFIERS = [ - "BerryModifier", - "PokemonInstantReviveModifier", - "TerastallizeModifier", - "PokemonBaseStatModifier", - "PokemonBaseStatTotalModifier", -]; +const OPTION_3_DISALLOWED_MODIFIERS = [HeldItemCategoryId.BERRY, HeldItemId.REVIVER_SEED]; const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2; +async function backupOption() { + globalScene.getPlayerPokemon()?.heldItemManager.add(HeldItemId.SHELL_BELL); + globalScene.playSound("item_fanfare"); + await showEncounterText( + i18next.t("battle:rewardGain", { + modifierName: allHeldItems[HeldItemId.SHELL_BELL].name, + }), + null, + undefined, + true, + ); + doEventReward(); +} + const doEventReward = () => { const event_buff = timedEventManager.getDelibirdyBuff(); if (event_buff.length > 0) { const candidates = event_buff.filter(c => { - const mtype = generateModifierType(modifierTypes[c]); - const existingCharm = globalScene.findModifier(m => m.type.id === mtype?.id); - return !(existingCharm && existingCharm.getStackCount() >= existingCharm.getMaxStackCount()); + const fullStack = globalScene.trainerItems.isMaxStack(c); + return !fullStack; }); if (candidates.length > 0) { globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes[randSeedItem(candidates)]); @@ -165,20 +164,11 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withOptionPhase(async () => { // Give the player an Amulet Coin // Check if the player has max stacks of that item already - const existing = globalScene.findModifier(m => m instanceof MoneyMultiplierModifier) as MoneyMultiplierModifier; + const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.AMULET_COIN); - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { + if (fullStack) { // At max stacks, give the first party pokemon a Shell Bell instead - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell); - globalScene.playSound("item_fanfare"); - await showEncounterText( - i18next.t("battle:rewardGain", { modifierName: shellBell.name }), - null, - undefined, - true, - ); - doEventReward(); + backupOption(); } else { globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.AMULET_COIN); doEventReward(); @@ -205,19 +195,17 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with const encounter = globalScene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones - const validItems = pokemon.getHeldItems().filter(it => { - return OPTION_2_ALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable; - }); + const validItems = pokemon.heldItemManager.filterRequestedItems(OPTION_2_ALLOWED_MODIFIERS, true); - return validItems.map((modifier: PokemonHeldItemModifier) => { + return validItems.map((item: HeldItemId) => { const option: OptionSelectItem = { - label: modifier.type.name, + label: allHeldItems[item].name, handler: () => { // Pokemon and item selected - encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.setDialogueToken("chosenItem", allHeldItems[item].name); encounter.misc = { chosenPokemon: pokemon, - chosenModifier: modifier, + chosenItem: item, }; return true; }, @@ -240,59 +228,35 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with }) .withOptionPhase(async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; - const modifier: BerryModifier | PokemonInstantReviveModifier = encounter.misc.chosenModifier; + const chosenItem: HeldItemId = encounter.misc.chosenItem; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed - if (modifier instanceof BerryModifier) { + if (isItemInCategory(chosenItem, HeldItemCategoryId.BERRY)) { // Check if the player has max stacks of that Candy Jar already - const existing = globalScene.findModifier( - m => m instanceof LevelIncrementBoosterModifier, - ) as LevelIncrementBoosterModifier; + const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.CANDY_JAR); - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { + if (fullStack) { // At max stacks, give the first party pokemon a Shell Bell instead - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell); - globalScene.playSound("item_fanfare"); - await showEncounterText( - i18next.t("battle:rewardGain", { - modifierName: shellBell.name, - }), - null, - undefined, - true, - ); - doEventReward(); + backupOption(); } else { globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.CANDY_JAR); doEventReward(); } } else { // Check if the player has max stacks of that Berry Pouch already - const existing = globalScene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; + const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.BERRY_POUCH); - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { + if (fullStack) { // At max stacks, give the first party pokemon a Shell Bell instead - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell); - globalScene.playSound("item_fanfare"); - await showEncounterText( - i18next.t("battle:rewardGain", { - modifierName: shellBell.name, - }), - null, - undefined, - true, - ); - doEventReward(); + backupOption(); } else { globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.BERRY_POUCH); doEventReward(); } } - chosenPokemon.loseHeldItem(modifier, false); + chosenPokemon.loseHeldItem(chosenItem, false); leaveEncounterWithoutBattle(true); }) @@ -315,21 +279,17 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with const encounter = globalScene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones - const validItems = pokemon.getHeldItems().filter(it => { - return ( - !OPTION_3_DISALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable - ); - }); + const validItems = pokemon.heldItemManager.filterRequestedItems(OPTION_3_DISALLOWED_MODIFIERS, true, true); - return validItems.map((modifier: PokemonHeldItemModifier) => { + return validItems.map((item: HeldItemId) => { const option: OptionSelectItem = { - label: modifier.type.name, + label: allHeldItems[item].name, handler: () => { // Pokemon and item selected - encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.setDialogueToken("chosenItem", allHeldItems[item].name); encounter.misc = { chosenPokemon: pokemon, - chosenModifier: modifier, + chosenItem: item, }; return true; }, @@ -356,20 +316,12 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Check if the player has max stacks of Healing Charm already - const existing = globalScene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { + const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.HEALING_CHARM); + + if (fullStack) { // At max stacks, give the first party pokemon a Shell Bell instead - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(globalScene.getPlayerParty()[0], shellBell); - globalScene.playSound("item_fanfare"); - await showEncounterText( - i18next.t("battle:rewardGain", { modifierName: shellBell.name }), - null, - undefined, - true, - ); - doEventReward(); + backupOption(); } else { globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.HEALING_CHARM); doEventReward(); diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index e8900f8def4..8ad013be6fe 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -7,9 +7,7 @@ import { setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals, - generateModifierType, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -35,7 +33,6 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun import { applyAbilityOverrideToPokemon, applyDamageToPokemon, - applyModifierTypeToPlayerPokemon, } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -45,8 +42,11 @@ import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Stat } from "#enums/stat"; import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { getNewHeldItemFromCategory } from "#app/items/held-item-pool"; +import { allHeldItems } from "#app/data/data-lists"; import { MoveUseMode } from "#enums/move-use-mode"; -import { allAbilities, modifierTypes } from "#app/data/data-lists"; +import { allAbilities } from "#app/data/data-lists"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/fieryFallout"; @@ -302,16 +302,14 @@ function giveLeadPokemonAttackTypeBoostItem() { const leadPokemon = globalScene.getPlayerParty()?.[0]; if (leadPokemon) { // Generate type booster held item, default to Charcoal if item fails to generate - let boosterModifierType = generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType; - if (!boosterModifierType) { - boosterModifierType = generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [ - PokemonType.FIRE, - ]) as AttackTypeBoosterModifierType; + let item = getNewHeldItemFromCategory(HeldItemCategoryId.TYPE_ATTACK_BOOSTER, leadPokemon); + if (!item) { + item = HeldItemId.CHARCOAL; } - applyModifierTypeToPlayerPokemon(leadPokemon, boosterModifierType); + leadPokemon.heldItemManager.add(item); const encounter = globalScene.currentBattle.mysteryEncounter!; - encounter.setDialogueToken("itemName", boosterModifierType.name); + encounter.setDialogueToken("itemName", allHeldItems[item].name); encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); queueEncounterMessage(`${namespace}:found_item`); } diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index c53ff610c48..038b3e69de0 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -9,7 +9,7 @@ import { } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import type Pokemon from "#app/field/pokemon"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { ModifierPoolType } from "#enums/modifier-pool-type"; @@ -89,12 +89,12 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder. // Waves 10-40 GREAT, 60-120 ULTRA, 120-160 ROGUE, 160-180 MASTER const tier = globalScene.currentBattle.waveIndex > 160 - ? ModifierTier.MASTER + ? RewardTier.MASTER : globalScene.currentBattle.waveIndex > 120 - ? ModifierTier.ROGUE + ? RewardTier.ROGUE : globalScene.currentBattle.waveIndex > 40 - ? ModifierTier.ULTRA - : ModifierTier.GREAT; + ? RewardTier.ULTRA + : RewardTier.GREAT; regenerateModifierPoolThresholds(globalScene.getPlayerParty(), ModifierPoolType.PLAYER, 0); let item: ModifierTypeOption | null = null; // TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index 9611560fe62..432cc3622fd 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -388,7 +388,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise { globalScene.add.existing(pokemon); globalScene.field.add(pokemon); addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.pokeball); - globalScene.updateModifiers(true); + globalScene.updateItems(true); globalScene.updateFieldScale(); pokemon.showInfo(); pokemon.playAnim(); diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 6bbc1a68772..3c3a8d29475 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -4,7 +4,6 @@ import { setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { TrainerSlot } from "#enums/trainer-slot"; -import { ModifierTier } from "#enums/modifier-tier"; import { MusicPreference } from "#app/system/settings/settings"; import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; @@ -33,13 +32,6 @@ import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { - HiddenAbilityRateBoosterModifier, - PokemonFormChangeItemModifier, - ShinyRateBoosterModifier, - SpeciesStatBoosterModifier, -} from "#app/modifier/modifier"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import PokemonData from "#app/system/pokemon-data"; import i18next from "i18next"; @@ -53,6 +45,11 @@ import type { PokeballType } from "#enums/pokeball"; import { doShinySparkleAnim } from "#app/field/anims"; import { TrainerType } from "#enums/trainer-type"; import { timedEventManager } from "#app/global-event-manager"; +import { HeldItemCategoryId, type HeldItemId, isItemInCategory } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; +import { RewardTier } from "#enums/reward-tier"; +import { getHeldItemTier } from "#app/items/held-item-tiers"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/globalTradeSystem"; @@ -215,9 +212,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil const encounter = globalScene.currentBattle.mysteryEncounter!; const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; - const modifiers = tradedPokemon - .getHeldItems() - .filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); + const heldItemConfig = tradedPokemon.heldItemManager + .generateHeldItemConfiguration() + .filter(ic => !isItemInCategory(ic.entry as HeldItemId, HeldItemCategoryId.SPECIES_STAT_BOOSTER)); // Generate a trainer name const traderName = generateRandomTraderName(); @@ -241,16 +238,12 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil dataSource.variant, dataSource.ivs, dataSource.nature, + heldItemConfig, dataSource, ); globalScene.getPlayerParty().push(newPlayerPokemon); await newPlayerPokemon.loadAssets(); - for (const mod of modifiers) { - mod.pokemonId = newPlayerPokemon.id; - globalScene.addModifier(mod, true, false, false, true); - } - // Show the trade animation await showTradeBackground(); await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon); @@ -283,7 +276,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil if (timedEventManager.isEventActive()) { shinyThreshold.value *= timedEventManager.getShinyMultiplier(); } - globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold }); // Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms // Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that @@ -297,7 +290,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil if (tradePokemon.species.abilityHidden) { if (tradePokemon.abilityIndex < hiddenIndex) { const hiddenAbilityChance = new NumberHolder(64); - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); @@ -336,9 +331,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil const encounter = globalScene.currentBattle.mysteryEncounter!; const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; - const modifiers = tradedPokemon - .getHeldItems() - .filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); + const heldItemConfig = tradedPokemon.heldItemManager + .generateHeldItemConfiguration() + .filter(ic => !isItemInCategory(ic.entry as HeldItemId, HeldItemCategoryId.SPECIES_STAT_BOOSTER)); // Generate a trainer name const traderName = generateRandomTraderName(); @@ -361,16 +356,12 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil dataSource.variant, dataSource.ivs, dataSource.nature, + heldItemConfig, dataSource, ); globalScene.getPlayerParty().push(newPlayerPokemon); await newPlayerPokemon.loadAssets(); - for (const mod of modifiers) { - mod.pokemonId = newPlayerPokemon.id; - globalScene.addModifier(mod, true, false, false, true); - } - // Show the trade animation await showTradeBackground(); await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon); @@ -395,17 +386,15 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil const encounter = globalScene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones - const validItems = pokemon.getHeldItems().filter(it => { - return it.isTransferable; - }); + const validItems = pokemon.heldItemManager.getTransferableHeldItems(); - return validItems.map((modifier: PokemonHeldItemModifier) => { + return validItems.map((id: HeldItemId) => { const option: OptionSelectItem = { - label: modifier.type.name, + label: allHeldItems[id].name, handler: () => { // Pokemon and item selected - encounter.setDialogueToken("chosenItem", modifier.type.name); - encounter.misc.chosenModifier = modifier; + encounter.setDialogueToken("chosenItem", allHeldItems[id].name); + encounter.misc.chosenHeldItem = id; encounter.misc.chosenPokemon = pokemon; return true; }, @@ -416,10 +405,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil const selectableFilter = (pokemon: Pokemon) => { // If pokemon has items to trade - const meetsReqs = - pokemon.getHeldItems().filter(it => { - return it.isTransferable; - }).length > 0; + const meetsReqs = pokemon.heldItemManager.getTransferableHeldItems().length > 0; if (!meetsReqs) { return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; } @@ -431,23 +417,15 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil }) .withOptionPhase(async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; - const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier; + const heldItemId = encounter.misc.chosenHeldItem as HeldItemId; const party = globalScene.getPlayerParty(); const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Check tier of the traded item, the received item will be one tier up - const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party); - let tier = type.tier ?? ModifierTier.GREAT; - // Eggs and White Herb are not in the pool - if (type.id === "WHITE_HERB") { - tier = ModifierTier.GREAT; - } else if (type.id === "LUCKY_EGG") { - tier = ModifierTier.ULTRA; - } else if (type.id === "GOLDEN_EGG") { - tier = ModifierTier.ROGUE; - } + let tier = getHeldItemTier(heldItemId) ?? RewardTier.GREAT; + // Increment tier by 1 - if (tier < ModifierTier.MASTER) { + if (tier < RewardTier.MASTER) { tier++; } @@ -467,8 +445,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil fillRemaining: false, }); - chosenPokemon.loseHeldItem(modifier, false); - await globalScene.updateModifiers(true, true); + chosenPokemon.heldItemManager.remove(heldItemId); + await globalScene.updateItems(true); // Generate a trainer name const traderName = generateRandomTraderName(); diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index ae3d905cf91..f109d9ad7ca 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -7,7 +7,7 @@ import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { trainerPartyTemplates } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; @@ -176,7 +176,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], + guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.GREAT, RewardTier.GREAT], fillRemaining: true, }); @@ -207,7 +207,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter encounter.expMultiplier = 0.9; setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], + guaranteedModifierTiers: [RewardTier.ROGUE, RewardTier.ROGUE, RewardTier.ULTRA, RewardTier.GREAT], fillRemaining: true, }); diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 321e65d7008..ca3c3e5aafd 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -16,7 +16,7 @@ import { } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { randSeedInt } from "#app/utils/common"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -144,7 +144,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT) { // Choose between 2 COMMON / 2 GREAT tier items (20%) setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.GREAT], + guaranteedModifierTiers: [RewardTier.COMMON, RewardTier.COMMON, RewardTier.GREAT, RewardTier.GREAT], }); // Display result message then proceed to rewards queueEncounterMessage(`${namespace}:option.1.normal`); @@ -152,7 +152,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde } else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT) { // Choose between 3 ULTRA tier items (30%) setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], + guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.ULTRA], }); // Display result message then proceed to rewards queueEncounterMessage(`${namespace}:option.1.good`); @@ -160,7 +160,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde } else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT - ROGUE_REWARDS_PERCENT) { // Choose between 2 ROGUE tier items (10%) setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], + guaranteedModifierTiers: [RewardTier.ROGUE, RewardTier.ROGUE], }); // Display result message then proceed to rewards queueEncounterMessage(`${namespace}:option.1.great`); @@ -171,7 +171,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde ) { // Choose 1 MASTER tier item (5%) setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.MASTER], + guaranteedModifierTiers: [RewardTier.MASTER], }); // Display result message then proceed to rewards queueEncounterMessage(`${namespace}:option.1.amazing`); diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 207e6ca400d..6d47959caac 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -11,7 +11,6 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { TrainerSlot } from "#enums/trainer-slot"; -import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; import type { EnemyPokemon } from "#app/field/pokemon"; import { PokeballType } from "#enums/pokeball"; import { PlayerGender } from "#enums/player-gender"; @@ -31,6 +30,8 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/safariZone"; @@ -297,7 +298,9 @@ async function summonSafariPokemon() { const hiddenIndex = pokemon.species.ability2 ? 2 : 1; if (pokemon.abilityIndex < hiddenIndex) { const hiddenAbilityChance = new NumberHolder(256); - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); @@ -332,8 +335,7 @@ async function summonSafariPokemon() { // shows up and the IV scanner breaks. For now, we place the IV scanner code // separately so that at least the IV scanner works. - const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); - if (ivScannerModifier) { + if (globalScene.trainerItems.hasItem(TrainerItemId.IV_SCANNER)) { globalScene.phaseManager.pushNew("ScanIvsPhase", pokemon.getBattlerIndex()); } } diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 967c105c740..cff759b5d60 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -1,5 +1,4 @@ import { - generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, @@ -7,7 +6,6 @@ import { } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { modifierTypes } from "#app/data/data-lists"; import { randSeedInt } from "#app/utils/common"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; @@ -19,7 +17,6 @@ import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { applyDamageToPokemon, - applyModifierTypeToPlayerPokemon, isPokemonValidForEncounterOptionSelection, } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -28,6 +25,8 @@ import type { Nature } from "#enums/nature"; import { getNatureName } from "#app/data/nature"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import i18next from "i18next"; +import { getNewVitaminHeldItem } from "#app/items/held-item-pool"; +import { allHeldItems } from "#app/data/data-lists"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/shadyVitaminDealer"; @@ -97,15 +96,12 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui // Update money updatePlayerMoney(-(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); // Calculate modifiers and dialogue tokens - const modifiers = [ - generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, - generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, - ]; - encounter.setDialogueToken("boost1", modifiers[0].name); - encounter.setDialogueToken("boost2", modifiers[1].name); + const items = [getNewVitaminHeldItem(), getNewVitaminHeldItem()]; + encounter.setDialogueToken("boost1", allHeldItems[items[0]].name); + encounter.setDialogueToken("boost2", allHeldItems[items[1]].name); encounter.misc = { chosenPokemon: pokemon, - modifiers: modifiers, + items: items, }; }; @@ -132,10 +128,10 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui // Choose Cheap Option const encounter = globalScene.currentBattle.mysteryEncounter!; const chosenPokemon = encounter.misc.chosenPokemon; - const modifiers = encounter.misc.modifiers; + const items = encounter.misc.items; - for (const modType of modifiers) { - await applyModifierTypeToPlayerPokemon(chosenPokemon, modType); + for (const item of items) { + chosenPokemon.heldItemManager.add(item); } leaveEncounterWithoutBattle(true); @@ -180,15 +176,12 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui // Update money updatePlayerMoney(-(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney); // Calculate modifiers and dialogue tokens - const modifiers = [ - generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, - generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, - ]; - encounter.setDialogueToken("boost1", modifiers[0].name); - encounter.setDialogueToken("boost2", modifiers[1].name); + const items = [getNewVitaminHeldItem(), getNewVitaminHeldItem()]; + encounter.setDialogueToken("boost1", allHeldItems[items[0]].name); + encounter.setDialogueToken("boost2", allHeldItems[items[1]].name); encounter.misc = { chosenPokemon: pokemon, - modifiers: modifiers, + items: items, }; }; @@ -203,10 +196,10 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui // Choose Expensive Option const encounter = globalScene.currentBattle.mysteryEncounter!; const chosenPokemon = encounter.misc.chosenPokemon; - const modifiers = encounter.misc.modifiers; + const items = encounter.misc.items; - for (const modType of modifiers) { - await applyModifierTypeToPlayerPokemon(chosenPokemon, modType); + for (const item of items) { + chosenPokemon.heldItemManager.add(item); } leaveEncounterWithoutBattle(true); diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index d6a85dee119..55cefeed695 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -1,5 +1,4 @@ import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; -import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; @@ -11,7 +10,6 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "../utils/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, @@ -27,10 +25,9 @@ import { AiType } from "#enums/ai-type"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { BerryType } from "#enums/berry-type"; -import { Stat } from "#enums/stat"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; import { randSeedInt } from "#app/utils/common"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveUseMode } from "#enums/move-use-mode"; /** i18n namespace for the encounter */ @@ -78,24 +75,12 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil status: [StatusEffect.SLEEP, 6], // Extra turns on timer for Snorlax's start of fight moves nature: Nature.DOCILE, moveSet: [MoveId.BODY_SLAM, MoveId.CRUNCH, MoveId.SLEEP_TALK, MoveId.REST], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType, - stackCount: randSeedInt(2, 0), - }, - { - modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType, - stackCount: randSeedInt(2, 0), - }, + heldItemConfig: [ + { entry: HeldItemId.SITRUS_BERRY, count: 1 }, + { entry: HeldItemId.ENIGMA_BERRY, count: 1 }, + { entry: HeldItemId.HP_UP, count: 1 }, + { entry: HeldItemId.SOOTHE_BELL, count: randSeedInt(2, 0) }, + { entry: HeldItemId.LUCKY_EGG, count: randSeedInt(2, 0) }, ], customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), aiType: AiType.SMART, // Required to ensure Snorlax uses Sleep Talk while it is asleep diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index 03c09f6918e..92469964853 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -614,7 +614,6 @@ function removePokemonFromPartyAndStoreHeldItems(encounter: MysteryEncounter, ch party[chosenIndex] = party[0]; party[0] = chosenPokemon; encounter.misc.originalParty = globalScene.getPlayerParty().slice(1); - encounter.misc.originalPartyHeldItems = encounter.misc.originalParty.map(p => p.getHeldItems()); globalScene["party"] = [chosenPokemon]; } @@ -623,14 +622,7 @@ function restorePartyAndHeldItems() { // Restore original party globalScene.getPlayerParty().push(...encounter.misc.originalParty); - // Restore held items - const originalHeldItems = encounter.misc.originalPartyHeldItems; - for (const pokemonHeldItemsList of originalHeldItems) { - for (const heldItem of pokemonHeldItemsList) { - globalScene.addModifier(heldItem, true, false, false, true); - } - } - globalScene.updateModifiers(true); + globalScene.updateItems(true); } function onGameOver() { diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index f85206bcbc6..670cd76f534 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -5,9 +5,7 @@ import { leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, - generateModifierType, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; @@ -19,15 +17,14 @@ import { Nature } from "#enums/nature"; import type Pokemon from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MoveId } from "#enums/move-id"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveUseMode } from "#enums/move-use-mode"; /** the i18n namespace for the encounter */ @@ -95,23 +92,12 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), nature: Nature.HARDY, moveSet: [MoveId.INFESTATION, MoveId.SALT_CURE, MoveId.GASTRO_ACID, MoveId.HEAL_ORDER], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, - stackCount: 2, - }, + heldItemConfig: [ + { entry: HeldItemId.SITRUS_BERRY, count: 1 }, + { entry: HeldItemId.ENIGMA_BERRY, count: 1 }, + { entry: HeldItemId.APICOT_BERRY, count: 1 }, + { entry: HeldItemId.GANLON_BERRY, count: 1 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ], tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { @@ -171,11 +157,11 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder sortedParty.forEach((pokemon, index) => { if (index < 2) { // -15 to the two highest BST mons - modifyPlayerPokemonBST(pokemon, false); + pokemon.heldItemManager.add(HeldItemId.SHUCKLE_JUICE_BAD); encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender()); } else { // +10 for the rest - modifyPlayerPokemonBST(pokemon, true); + pokemon.heldItemManager.add(HeldItemId.SHUCKLE_JUICE_GOOD); } }); diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 6d28a710953..a19335dd643 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -1,14 +1,11 @@ import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { - generateModifierType, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -20,17 +17,18 @@ import { AbilityId } from "#enums/ability-id"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { MoveId } from "#enums/move-id"; import { Nature } from "#enums/nature"; -import { PokemonType } from "#enums/pokemon-type"; -import { BerryType } from "#enums/berry-type"; -import { Stat } from "#enums/stat"; import { SpeciesFormChangeAbilityTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import i18next from "i18next"; -import { ModifierTier } from "#enums/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { modifierTypes } from "#app/data/data-lists"; +import { RewardTier } from "#enums/reward-tier"; +import { HeldItemId } from "#enums/held-item-id"; + +//TODO: make all items unstealable /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theWinstrateChallenge"; @@ -166,7 +164,7 @@ async function spawnNextTrainerOrEndEncounter() { await showEncounterDialogue(`${namespace}:victory_2`, `${namespace}:speaker`); globalScene.ui.clearText(); // Clears "Winstrate" title from screen as rewards get animated in const machoBrace = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_MACHO_BRACE)!; - machoBrace.type.tier = ModifierTier.MASTER; + machoBrace.type.tier = RewardTier.MASTER; setEncounterRewards({ guaranteedModifierTypeOptions: [machoBrace], fillRemaining: false, @@ -258,16 +256,9 @@ function getVictorTrainerConfig(): EnemyPartyConfig { abilityIndex: 0, // Guts nature: Nature.ADAMANT, moveSet: [MoveId.FACADE, MoveId.BRAVE_BIRD, MoveId.PROTECT, MoveId.QUICK_ATTACK], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, - isTransferable: false, - }, - { - modifier: generateModifierType(modifierTypes.FOCUS_BAND) as PokemonHeldItemModifierType, - stackCount: 2, - isTransferable: false, - }, + heldItemConfig: [ + { entry: HeldItemId.FLAME_ORB, count: 1 }, + { entry: HeldItemId.FOCUS_BAND, count: 2 }, ], }, { @@ -276,16 +267,9 @@ function getVictorTrainerConfig(): EnemyPartyConfig { abilityIndex: 1, // Guts nature: Nature.ADAMANT, moveSet: [MoveId.FACADE, MoveId.OBSTRUCT, MoveId.NIGHT_SLASH, MoveId.FIRE_PUNCH], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, - isTransferable: false, - }, - { - modifier: generateModifierType(modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType, - stackCount: 2, - isTransferable: false, - }, + heldItemConfig: [ + { entry: HeldItemId.FLAME_ORB, count: 1 }, + { entry: HeldItemId.LEFTOVERS, count: 2 }, ], }, ], @@ -302,16 +286,9 @@ function getVictoriaTrainerConfig(): EnemyPartyConfig { abilityIndex: 0, // Natural Cure nature: Nature.CALM, moveSet: [MoveId.SYNTHESIS, MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.SLEEP_POWDER], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType, - isTransferable: false, - }, - { - modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, - stackCount: 2, - isTransferable: false, - }, + heldItemConfig: [ + { entry: HeldItemId.SOUL_DEW, count: 1 }, + { entry: HeldItemId.QUICK_CLAW, count: 2 }, ], }, { @@ -320,21 +297,9 @@ function getVictoriaTrainerConfig(): EnemyPartyConfig { formIndex: 1, nature: Nature.TIMID, moveSet: [MoveId.PSYSHOCK, MoveId.MOONBLAST, MoveId.SHADOW_BALL, MoveId.WILL_O_WISP], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [ - PokemonType.PSYCHIC, - ]) as PokemonHeldItemModifierType, - stackCount: 1, - isTransferable: false, - }, - { - modifier: generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [ - PokemonType.FAIRY, - ]) as PokemonHeldItemModifierType, - stackCount: 1, - isTransferable: false, - }, + heldItemConfig: [ + { entry: HeldItemId.TWISTED_SPOON, count: 1 }, + { entry: HeldItemId.FAIRY_FEATHER, count: 1 }, ], }, ], @@ -351,17 +316,9 @@ function getViviTrainerConfig(): EnemyPartyConfig { abilityIndex: 3, // Lightning Rod nature: Nature.ADAMANT, moveSet: [MoveId.WATERFALL, MoveId.MEGAHORN, MoveId.KNOCK_OFF, MoveId.REST], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, - stackCount: 2, - isTransferable: false, - }, - { - modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, - stackCount: 4, - isTransferable: false, - }, + heldItemConfig: [ + { entry: HeldItemId.LUM_BERRY, count: 2 }, + { entry: HeldItemId.HP_UP, count: 4 }, ], }, { @@ -370,16 +327,9 @@ function getViviTrainerConfig(): EnemyPartyConfig { abilityIndex: 1, // Poison Heal nature: Nature.JOLLY, moveSet: [MoveId.SPORE, MoveId.SWORDS_DANCE, MoveId.SEED_BOMB, MoveId.DRAIN_PUNCH], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, - stackCount: 4, - isTransferable: false, - }, - { - modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType, - isTransferable: false, - }, + heldItemConfig: [ + { entry: HeldItemId.HP_UP, count: 4 }, + { entry: HeldItemId.TOXIC_ORB, count: 1 }, ], }, { @@ -388,13 +338,7 @@ function getViviTrainerConfig(): EnemyPartyConfig { formIndex: 1, nature: Nature.CALM, moveSet: [MoveId.EARTH_POWER, MoveId.FIRE_BLAST, MoveId.YAWN, MoveId.PROTECT], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, - stackCount: 3, - isTransferable: false, - }, - ], + heldItemConfig: [{ entry: HeldItemId.QUICK_CLAW, count: 3 }], }, ], }; @@ -410,12 +354,7 @@ function getVickyTrainerConfig(): EnemyPartyConfig { formIndex: 1, nature: Nature.IMPISH, moveSet: [MoveId.AXE_KICK, MoveId.ICE_PUNCH, MoveId.ZEN_HEADBUTT, MoveId.BULLET_PUNCH], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType, - isTransferable: false, - }, - ], + heldItemConfig: [{ entry: HeldItemId.SHELL_BELL, count: 1 }], }, ], }; @@ -431,13 +370,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig { abilityIndex: 0, // Soundproof nature: Nature.MODEST, moveSet: [MoveId.THUNDERBOLT, MoveId.GIGA_DRAIN, MoveId.FOUL_PLAY, MoveId.THUNDER_WAVE], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.SPD]) as PokemonHeldItemModifierType, - stackCount: 2, - isTransferable: false, - }, - ], + heldItemConfig: [{ entry: HeldItemId.ZINC, count: 2 }], }, { species: getPokemonSpecies(SpeciesId.SWALOT), @@ -445,51 +378,18 @@ function getVitoTrainerConfig(): EnemyPartyConfig { abilityIndex: 2, // Gluttony nature: Nature.QUIET, moveSet: [MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.ICE_BEAM, MoveId.EARTHQUAKE], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.STARF]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SALAC]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LANSAT]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LIECHI]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.PETAYA]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, - stackCount: 2, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LEPPA]) as PokemonHeldItemModifierType, - stackCount: 2, - }, + heldItemConfig: [ + { entry: HeldItemId.SITRUS_BERRY, count: 2 }, + { entry: HeldItemId.APICOT_BERRY, count: 2 }, + { entry: HeldItemId.GANLON_BERRY, count: 2 }, + { entry: HeldItemId.STARF_BERRY, count: 2 }, + { entry: HeldItemId.SALAC_BERRY, count: 2 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, + { entry: HeldItemId.LANSAT_BERRY, count: 2 }, + { entry: HeldItemId.LIECHI_BERRY, count: 2 }, + { entry: HeldItemId.PETAYA_BERRY, count: 2 }, + { entry: HeldItemId.ENIGMA_BERRY, count: 2 }, + { entry: HeldItemId.LEPPA_BERRY, count: 2 }, ], }, { @@ -498,13 +398,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig { abilityIndex: 2, // Tangled Feet nature: Nature.JOLLY, moveSet: [MoveId.DRILL_PECK, MoveId.QUICK_ATTACK, MoveId.THRASH, MoveId.KNOCK_OFF], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.KINGS_ROCK) as PokemonHeldItemModifierType, - stackCount: 2, - isTransferable: false, - }, - ], + heldItemConfig: [{ entry: HeldItemId.KINGS_ROCK, count: 2 }], }, { species: getPokemonSpecies(SpeciesId.ALAKAZAM), @@ -512,13 +406,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig { formIndex: 1, nature: Nature.BOLD, moveSet: [MoveId.PSYCHIC, MoveId.SHADOW_BALL, MoveId.FOCUS_BLAST, MoveId.THUNDERBOLT], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.WIDE_LENS) as PokemonHeldItemModifierType, - stackCount: 2, - isTransferable: false, - }, - ], + heldItemConfig: [{ entry: HeldItemId.WIDE_LENS, count: 2 }], }, { species: getPokemonSpecies(SpeciesId.DARMANITAN), @@ -526,13 +414,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig { abilityIndex: 0, // Sheer Force nature: Nature.IMPISH, moveSet: [MoveId.EARTHQUAKE, MoveId.U_TURN, MoveId.FLARE_BLITZ, MoveId.ROCK_SLIDE], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, - stackCount: 2, - isTransferable: false, - }, - ], + heldItemConfig: [{ entry: HeldItemId.QUICK_CLAW, count: 2 }], }, ], }; diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index f76bd66151f..1f86cf31f8f 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -11,7 +11,6 @@ import { getNatureName } from "#app/data/nature"; import { speciesStarterCosts } from "#app/data/balance/starters"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { AbilityAttr } from "#enums/ability-attr"; import PokemonData from "#app/system/pokemon-data"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; @@ -25,7 +24,6 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import type HeldModifierConfig from "#app/@types/held-modifier-config"; import i18next from "i18next"; import { getStatKey } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; @@ -102,8 +100,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Spawn light training session with chosen pokemon // Every 50 waves, add +1 boss segment, capping at 5 const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 50), 5); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(playerPokemon, segments, modifiers); + const config = getEnemyConfig(playerPokemon, segments); globalScene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { @@ -152,13 +149,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde globalScene.gameData.setPokemonCaught(playerPokemon, false); } - // Add pokemon and mods back - globalScene.getPlayerParty().push(playerPokemon); - for (const mod of modifiers.value) { - mod.pokemonId = playerPokemon.id; - globalScene.addModifier(mod, true, false, false, true); - } - globalScene.updateModifiers(true); + // Make held items show up again + globalScene.updateItems(true); queueEncounterMessage(`${namespace}:option.1.finished`); }; @@ -217,8 +209,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Spawn medium training session with chosen pokemon // Every 40 waves, add +1 boss segment, capping at 6 const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 40), 6); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(playerPokemon, segments, modifiers); + const config = getEnemyConfig(playerPokemon, segments); globalScene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { @@ -227,13 +218,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde playerPokemon.setCustomNature(encounter.misc.chosenNature); globalScene.gameData.unlockSpeciesNature(playerPokemon.species, encounter.misc.chosenNature); - // Add pokemon and modifiers back - globalScene.getPlayerParty().push(playerPokemon); - for (const mod of modifiers.value) { - mod.pokemonId = playerPokemon.id; - globalScene.addModifier(mod, true, false, false, true); - } - globalScene.updateModifiers(true); + // Make held items show up again + globalScene.updateItems(true); }; setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase); @@ -308,8 +294,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Every 30 waves, add +1 boss segment, capping at 6 // Also starts with +1 to all stats const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 30), 6); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(playerPokemon, segments, modifiers); + const config = getEnemyConfig(playerPokemon, segments); config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; globalScene.removePokemonFromPlayerParty(playerPokemon, false); @@ -340,13 +325,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde playerPokemon.calculateStats(); globalScene.gameData.setPokemonCaught(playerPokemon, false); - // Add pokemon and mods back - globalScene.getPlayerParty().push(playerPokemon); - for (const mod of modifiers.value) { - mod.pokemonId = playerPokemon.id; - globalScene.addModifier(mod, true, false, false, true); - } - globalScene.updateModifiers(true); + // Make held items show up again + globalScene.updateItems(true); }; setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase); @@ -373,18 +353,12 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde ) .build(); -function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number, modifiers: ModifiersHolder): EnemyPartyConfig { +function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number): EnemyPartyConfig { playerPokemon.resetSummonData(); // Passes modifiers by reference - modifiers.value = playerPokemon.getHeldItems(); - const modifierConfigs = modifiers.value.map(mod => { - return { - modifier: mod.clone(), - isTransferable: false, - stackCount: mod.stackCount, - }; - }) as HeldModifierConfig[]; + // TODO: fix various things, like make enemy items untransferable, make sure form change items can come back + const config = playerPokemon.heldItemManager.generateHeldItemConfiguration(); const data = new PokemonData(playerPokemon); return { @@ -396,12 +370,8 @@ function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number, modifier formIndex: playerPokemon.formIndex, level: playerPokemon.level, dataSource: data, - modifierConfigs: modifierConfigs, + heldItemConfig: config, }, ], }; } - -class ModifiersHolder { - public value: PokemonHeldItemModifier[] = []; -} diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index e2a740c4900..34bdaa86249 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -1,14 +1,12 @@ import type { EnemyPartyConfig, EnemyPokemonConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { - generateModifierType, + assignItemToFirstFreePokemon, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/data/data-lists"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -17,11 +15,9 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { SpeciesId } from "#enums/species-id"; -import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#app/modifier/modifier"; -import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import i18next from "#app/plugins/i18n"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { MoveId } from "#enums/move-id"; import { BattlerIndex } from "#enums/battler-index"; @@ -29,6 +25,10 @@ import { PokemonMove } from "#app/data/moves/pokemon-move"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { randSeedInt } from "#app/utils/common"; import { MoveUseMode } from "#enums/move-use-mode"; +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { allTrainerItems } from "#app/data/data-lists"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/trashToTreasure"; @@ -83,41 +83,13 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde formIndex: 1, // Gmax bossSegmentModifier: 1, // +1 Segment from normal moveSet: [MoveId.GUNK_SHOT, MoveId.STOMPING_TANTRUM, MoveId.HAMMER_ARM, MoveId.PAYBACK], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType, - stackCount: randSeedInt(2, 0), - }, - { - modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType, - stackCount: randSeedInt(2, 1), - }, - { - modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType, - stackCount: randSeedInt(3, 1), - }, - { - modifier: generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType, - stackCount: randSeedInt(2, 0), - }, + heldItemConfig: [ + { entry: HeldItemCategoryId.BERRY, count: 4 }, + { entry: HeldItemCategoryId.BASE_STAT_BOOST, count: 2 }, + { entry: HeldItemId.TOXIC_ORB, count: randSeedInt(2, 0) }, + { entry: HeldItemId.SOOTHE_BELL, count: randSeedInt(2, 1) }, + { entry: HeldItemId.LUCKY_EGG, count: randSeedInt(3, 1) }, + { entry: HeldItemId.GOLDEN_EGG, count: randSeedInt(2, 0) }, ], }; const config: EnemyPartyConfig = { @@ -157,18 +129,14 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde await transitionMysteryEncounterIntroVisuals(); await tryApplyDigRewardItems(); - const blackSludge = generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ - SHOP_ITEM_COST_MULTIPLIER, - ]); - const modifier = blackSludge?.newModifier(); - if (modifier) { - await globalScene.addModifier(modifier, false, false, false, true); + const blackSludge = globalScene.trainerItems.add(TrainerItemId.BLACK_SLUDGE); + if (blackSludge) { globalScene.playSound("battle_anims/PRSFX- Venom Drench", { volume: 2, }); await showEncounterText( i18next.t("battle:rewardGain", { - modifierName: modifier.type.name, + modifierName: allTrainerItems[TrainerItemId.BLACK_SLUDGE].name, }), null, undefined, @@ -200,7 +168,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde const encounter = globalScene.currentBattle.mysteryEncounter!; setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], + guaranteedModifierTiers: [RewardTier.ROGUE, RewardTier.ROGUE, RewardTier.ULTRA, RewardTier.GREAT], fillRemaining: true, }); encounter.startOfBattleEffects.push( @@ -224,44 +192,18 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde .build(); async function tryApplyDigRewardItems() { - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - const leftovers = generateModifierType(modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType; - const party = globalScene.getPlayerParty(); - // Iterate over the party until an item was successfully given // First leftovers - for (const pokemon of party) { - const heldItems = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, - true, - ) as PokemonHeldItemModifier[]; - const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier; - - if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount()) { - await applyModifierTypeToPlayerPokemon(pokemon, leftovers); - break; - } - } + assignItemToFirstFreePokemon(HeldItemId.LEFTOVERS, party); // Second leftovers - for (const pokemon of party) { - const heldItems = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, - true, - ) as PokemonHeldItemModifier[]; - const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier; - - if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount()) { - await applyModifierTypeToPlayerPokemon(pokemon, leftovers); - break; - } - } + assignItemToFirstFreePokemon(HeldItemId.LEFTOVERS, party); globalScene.playSound("item_fanfare"); await showEncounterText( i18next.t("battle:rewardGainCount", { - modifierName: leftovers.name, + modifierName: allHeldItems[HeldItemId.LEFTOVERS].name, count: 2, }), null, @@ -270,23 +212,12 @@ async function tryApplyDigRewardItems() { ); // Only Shell bell - for (const pokemon of party) { - const heldItems = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, - true, - ) as PokemonHeldItemModifier[]; - const existingShellBell = heldItems.find(m => m instanceof HitHealModifier) as HitHealModifier; - - if (!existingShellBell || existingShellBell.getStackCount() < existingShellBell.getMaxStackCount()) { - await applyModifierTypeToPlayerPokemon(pokemon, shellBell); - break; - } - } + assignItemToFirstFreePokemon(HeldItemId.SHELL_BELL, party); globalScene.playSound("item_fanfare"); await showEncounterText( i18next.t("battle:rewardGainCount", { - modifierName: shellBell.name, + modifierName: allHeldItems[HeldItemId.SHELL_BELL].name, count: 1, }), null, diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index 44e578540dd..7e5cc9568b1 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -1,6 +1,7 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { + getPartyBerries, getRandomEncounterSpecies, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, @@ -15,10 +16,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; -import { - MoveRequirement, - PersistentModifierRequirement, -} from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { HeldItemRequirement, MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { @@ -33,10 +31,11 @@ import { BattlerIndex } from "#enums/battler-index"; import { PokeballType } from "#enums/pokeball"; import { BattlerTagType } from "#enums/battler-tag-type"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { BerryModifier } from "#app/modifier/modifier"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { MoveUseMode } from "#enums/move-use-mode"; +import type { PokemonItemMap } from "#app/items/held-item-data-types"; +import { HeldItemCategoryId } from "#enums/held-item-id"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/uncommonBreed"; @@ -191,7 +190,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. ) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) - .withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withSceneRequirement(new HeldItemRequirement(HeldItemCategoryId.BERRY, 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, @@ -207,19 +206,18 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. // Remove 4 random berries from player's party // Get all player berry items, remove from party, and store reference - const berryItems: BerryModifier[] = globalScene.findModifiers( - m => m instanceof BerryModifier, - ) as BerryModifier[]; + + const berryMap = getPartyBerries(); + const stolenBerryMap: PokemonItemMap[] = []; + for (let i = 0; i < 4; i++) { - const index = randSeedInt(berryItems.length); - const randBerry = berryItems[index]; - randBerry.stackCount--; - if (randBerry.stackCount === 0) { - globalScene.removeModifier(randBerry); - berryItems.splice(index, 1); - } + const index = randSeedInt(berryMap.length); + const randBerry = berryMap[index]; + globalScene.getPokemonById(randBerry.pokemonId)?.heldItemManager.remove(randBerry.item.id); + stolenBerryMap.push(randBerry); + berryMap.splice(index, 1); } - await globalScene.updateModifiers(true, true); + await globalScene.updateItems(true); // Pokemon joins the team, with 2 egg moves const encounter = globalScene.currentBattle.mysteryEncounter!; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 9739f71cb0f..68aac8d9d49 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -7,7 +7,6 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "../utils/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, @@ -21,11 +20,8 @@ import { NumberHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "# import type PokemonSpecies from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { allSpecies } from "#app/data/data-lists"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#app/modifier/modifier"; import { achvs } from "#app/system/achv"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import { @@ -34,15 +30,18 @@ import { } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { getLevelTotalExp } from "#app/data/exp"; import { Challenges } from "#enums/challenges"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { PlayerGender } from "#enums/player-gender"; import { TrainerType } from "#enums/trainer-type"; import PokemonData from "#app/system/pokemon-data"; import { Nature } from "#enums/nature"; -import type HeldModifierConfig from "#app/@types/held-modifier-config"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { PartyMemberStrength } from "#enums/party-member-strength"; +import type { HeldItemConfiguration, HeldItemSpecs } from "#app/items/held-item-data-types"; +import { assignItemsFromConfiguration } from "#app/items/held-item-pool"; +import { HeldItemId } from "#enums/held-item-id"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/weirdDream"; @@ -262,20 +261,14 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit dataSource.player = false; // Copy held items to new pokemon - const newPokemonHeldItemConfigs: HeldModifierConfig[] = []; - for (const item of transformation.heldItems) { - newPokemonHeldItemConfigs.push({ - modifier: item.clone() as PokemonHeldItemModifier, - stackCount: item.getStackCount(), - isTransferable: false, - }); - } + // TODO: Make items untransferable + const newPokemonHeldItemConfig = transformation.heldItems; + // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats if (shouldGetOldGateau(newPokemon)) { - newPokemonHeldItemConfigs.push({ - modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU) as PokemonHeldItemModifierType, - stackCount: 1, - isTransferable: false, + newPokemonHeldItemConfig.push({ + entry: HeldItemId.OLD_GATEAU, + count: 1, }); } @@ -284,7 +277,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD, level: previousPokemon.level, dataSource: dataSource, - modifierConfigs: newPokemonHeldItemConfigs, + heldItemConfig: newPokemonHeldItemConfig, }; enemyPokemonConfigs.push(enemyConfig); @@ -316,12 +309,12 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit setEncounterRewards( { guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.GREAT, - ModifierTier.GREAT, + RewardTier.ROGUE, + RewardTier.ROGUE, + RewardTier.ULTRA, + RewardTier.ULTRA, + RewardTier.GREAT, + RewardTier.GREAT, ], fillRemaining: false, }, @@ -365,7 +358,7 @@ interface PokemonTransformation { previousPokemon: PlayerPokemon; newSpecies: PokemonSpecies; newPokemon: PlayerPokemon; - heldItems: PokemonHeldItemModifier[]; + heldItems: HeldItemConfiguration; } function getTeamTransformations(): PokemonTransformation[] { @@ -390,9 +383,7 @@ function getTeamTransformations(): PokemonTransformation[] { for (let i = 0; i < numPokemon; i++) { const removed = removedPokemon[i]; const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); - pokemonTransformations[index].heldItems = removed - .getHeldItems() - .filter(m => !(m instanceof PokemonFormChangeItemModifier)); + pokemonTransformations[index].heldItems = removed.heldItemManager.generateHeldItemConfiguration(); const bst = removed.getSpeciesForm().getBaseStatTotal(); let newBstRange: [number, number]; @@ -448,17 +439,14 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) { } // Copy old items to new pokemon - for (const item of transformation.heldItems) { - item.pokemonId = newPokemon.id; - globalScene.addModifier(item, false, false, false, true); - } + const heldItemConfiguration = transformation.heldItems; + // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats if (shouldGetOldGateau(newPokemon)) { - const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU(); - const modifier = modType?.newModifier(newPokemon); - if (modifier) { - globalScene.addModifier(modifier, false, false, false, true); - } + heldItemConfiguration.push({ + entry: HeldItemId.OLD_GATEAU, + count: 1, + }); } newPokemon.calculateStats(); @@ -499,7 +487,9 @@ async function postProcessTransformedPokemon( const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; if (newPokemon.abilityIndex < hiddenIndex) { const hiddenAbilityChance = new NumberHolder(256); - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 07fd155b2b2..ae4ae7ee02c 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -8,14 +8,14 @@ import { StatusEffect } from "#enums/status-effect"; import { PokemonType } from "#enums/pokemon-type"; import { WeatherType } from "#enums/weather-type"; import type { PlayerPokemon } from "#app/field/pokemon"; -import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; -import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; import { coerceArray, isNullOrUndefined } from "#app/utils/common"; import type { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { TimeOfDay } from "#enums/time-of-day"; +import type { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; export interface EncounterRequirement { meetsRequirement(): boolean; // Boolean to see if a requirement is met @@ -351,39 +351,6 @@ export class PartySizeRequirement extends EncounterSceneRequirement { } } -export class PersistentModifierRequirement extends EncounterSceneRequirement { - requiredHeldItemModifiers: string[]; - minNumberOfItems: number; - - constructor(heldItem: string | string[], minNumberOfItems = 1) { - super(); - this.minNumberOfItems = minNumberOfItems; - this.requiredHeldItemModifiers = coerceArray(heldItem); - } - - override meetsRequirement(): boolean { - const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) { - return false; - } - let modifierCount = 0; - for (const modifier of this.requiredHeldItemModifiers) { - const matchingMods = globalScene.findModifiers(m => m.constructor.name === modifier); - if (matchingMods?.length > 0) { - for (const matchingMod of matchingMods) { - modifierCount += matchingMod.stackCount; - } - } - } - - return modifierCount >= this.minNumberOfItems; - } - - override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] { - return ["requiredItem", this.requiredHeldItemModifiers[0]]; - } -} - export class MoneyRequirement extends EncounterSceneRequirement { requiredMoney: number; // Static value scalingMultiplier: number; // Calculates required money based off wave index @@ -833,72 +800,13 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen } export class HeldItemRequirement extends EncounterPokemonRequirement { - requiredHeldItemModifiers: string[]; - minNumberOfPokemon: number; - invertQuery: boolean; - requireTransferable: boolean; - - constructor(heldItem: string | string[], minNumberOfPokemon = 1, invertQuery = false, requireTransferable = true) { - super(); - this.minNumberOfPokemon = minNumberOfPokemon; - this.invertQuery = invertQuery; - this.requiredHeldItemModifiers = coerceArray(heldItem); - this.requireTransferable = requireTransferable; - } - - override meetsRequirement(): boolean { - const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon)) { - return false; - } - return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; - } - - override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { - if (!this.invertQuery) { - return partyPokemon.filter(pokemon => - this.requiredHeldItemModifiers.some(heldItem => { - return pokemon.getHeldItems().some(it => { - return it.constructor.name === heldItem && (!this.requireTransferable || it.isTransferable); - }); - }), - ); - } - // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers - // E.g. functions as a blacklist - return partyPokemon.filter( - pokemon => - pokemon.getHeldItems().filter(it => { - return ( - !this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem) && - (!this.requireTransferable || it.isTransferable) - ); - }).length > 0, - ); - } - - override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { - const requiredItems = pokemon?.getHeldItems().filter(it => { - return ( - this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem) && - (!this.requireTransferable || it.isTransferable) - ); - }); - if (requiredItems && requiredItems.length > 0) { - return ["heldItem", requiredItems[0].type.name]; - } - return ["heldItem", ""]; - } -} - -export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRequirement { - requiredHeldItemTypes: PokemonType[]; + requiredHeldItems: HeldItemId[] | HeldItemCategoryId[]; minNumberOfPokemon: number; invertQuery: boolean; requireTransferable: boolean; constructor( - heldItemTypes: PokemonType | PokemonType[], + heldItem: HeldItemId | HeldItemId[] | HeldItemCategoryId | HeldItemCategoryId[], minNumberOfPokemon = 1, invertQuery = false, requireTransferable = true, @@ -906,7 +814,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredHeldItemTypes = coerceArray(heldItemTypes); + this.requiredHeldItems = coerceArray(heldItem); this.requireTransferable = requireTransferable; } @@ -921,14 +829,9 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { if (!this.invertQuery) { return partyPokemon.filter(pokemon => - this.requiredHeldItemTypes.some(heldItemType => { - return pokemon.getHeldItems().some(it => { - return ( - it instanceof AttackTypeBoosterModifier && - (it.type as AttackTypeBoosterModifierType).moveType === heldItemType && - (!this.requireTransferable || it.isTransferable) - ); - }); + this.requiredHeldItems.some(heldItem => { + (pokemon.heldItemManager.hasItem(heldItem) || pokemon.heldItemManager.hasItemCategory(heldItem)) && + (!this.requireTransferable || allHeldItems[heldItem].isTransferable); }), ); } @@ -936,30 +839,24 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe // E.g. functions as a blacklist return partyPokemon.filter( pokemon => - pokemon.getHeldItems().filter(it => { - return !this.requiredHeldItemTypes.some( - heldItemType => - it instanceof AttackTypeBoosterModifier && - (it.type as AttackTypeBoosterModifierType).moveType === heldItemType && - (!this.requireTransferable || it.isTransferable), + pokemon.getHeldItems().filter(item => { + return ( + !this.requiredHeldItems.some(heldItem => item === heldItem) && + (!this.requireTransferable || allHeldItems[item].isTransferable) ); }).length > 0, ); } override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { - const requiredItems = pokemon?.getHeldItems().filter(it => { + const requiredItems = pokemon?.getHeldItems().filter(item => { return ( - this.requiredHeldItemTypes.some( - heldItemType => - it instanceof AttackTypeBoosterModifier && - (it.type as AttackTypeBoosterModifierType).moveType === heldItemType, - ) && - (!this.requireTransferable || it.isTransferable) + this.requiredHeldItems.some(heldItem => item === heldItem) && + (!this.requireTransferable || allHeldItems[item].isTransferable) ); }); if (requiredItems && requiredItems.length > 0) { - return ["heldItem", requiredItems[0].type.name]; + return ["heldItem", allHeldItems[requiredItems[0]].name]; } return ["heldItem", ""]; } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index bd2dfa998f4..71d02ced5d2 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,5 +1,4 @@ import type Battle from "#app/battle"; -import { BattleType } from "#enums/battle-type"; import { biomeLinks, BiomePoolTier } from "#app/data/balance/biomes"; import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/constants"; @@ -11,12 +10,7 @@ import { EnemyPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { FieldPosition } from "#enums/field-position"; import type { CustomModifierSettings, ModifierType } from "#app/modifier/modifier-type"; -import { - getPartyLuckValue, - ModifierTypeGenerator, - ModifierTypeOption, - regenerateModifierPoolThresholds, -} from "#app/modifier/modifier-type"; +import { getPartyLuckValue, ModifierTypeGenerator, ModifierTypeOption } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import { ModifierPoolType } from "#enums/modifier-pool-type"; import type PokemonData from "#app/system/pokemon-data"; @@ -44,7 +38,6 @@ import type PokemonSpecies from "#app/data/pokemon-species"; import type { IEggOptions } from "#app/data/egg"; import { Egg } from "#app/data/egg"; import type { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; -import type HeldModifierConfig from "#app/@types/held-modifier-config"; import type { Variant } from "#app/sprites/variant"; import { StatusEffect } from "#enums/status-effect"; import { globalScene } from "#app/global-scene"; @@ -53,6 +46,9 @@ import { PokemonType } from "#enums/pokemon-type"; import { getNatureName } from "#app/data/nature"; import { getPokemonNameWithAffix } from "#app/messages"; import { timedEventManager } from "#app/global-event-manager"; +import type { HeldItemConfiguration, PokemonItemMap } from "#app/items/held-item-data-types"; +import { HeldItemCategoryId, type HeldItemId, isItemInCategory } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; /** * Animates exclamation sprite over trainer's head at start of encounter @@ -102,7 +98,7 @@ export interface EnemyPokemonConfig { /** Can set just the status, or pass a timer on the status turns */ status?: StatusEffect | [StatusEffect, number]; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; - modifierConfigs?: HeldModifierConfig[]; + heldItemConfig?: HeldItemConfiguration; tags?: BattlerTagType[]; dataSource?: PokemonData; tera?: PokemonType; @@ -200,6 +196,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): battle.enemyLevels.forEach((level, e) => { let enemySpecies: PokemonSpecies | undefined; + let heldItemConfig: HeldItemConfiguration = []; let dataSource: PokemonData | undefined; let isBoss = false; if (!loaded) { @@ -211,12 +208,14 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): dataSource = config.dataSource; enemySpecies = config.species; isBoss = config.isBoss; + heldItemConfig = config.heldItemConfig ?? []; battle.enemyParty[e] = globalScene.addEnemyPokemon( enemySpecies, level, TrainerSlot.TRAINER, isBoss, false, + heldItemConfig, dataSource, ); } else { @@ -226,6 +225,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { const config = partyConfig.pokemonConfigs[e]; level = config.level ? config.level : level; + heldItemConfig = config.heldItemConfig ?? []; dataSource = config.dataSource; enemySpecies = config.species; isBoss = config.isBoss; @@ -242,6 +242,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): TrainerSlot.NONE, isBoss, false, + heldItemConfig, dataSource, ); } @@ -428,16 +429,6 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): enemyPokemon_2.x += 300; } }); - if (!loaded) { - regenerateModifierPoolThresholds( - globalScene.getEnemyField(), - battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, - ); - const customModifierTypes = partyConfig?.pokemonConfigs - ?.filter(config => config?.modifierConfigs) - .map(config => config.modifierConfigs!); - globalScene.generateEnemyModifiers(customModifierTypes); - } } /** @@ -1305,3 +1296,29 @@ export function calculateRareSpawnAggregateStats(luckValue: number) { console.log(stats); } + +// Iterate over the party until an item is successfully given +export function assignItemToFirstFreePokemon(item: HeldItemId, party: Pokemon[]): void { + for (const pokemon of party) { + const stack = pokemon.heldItemManager.getStack(item); + if (stack < allHeldItems[item].getMaxStackCount()) { + pokemon.heldItemManager.add(item); + return; + } + } +} + +// Creates an item map of berries to pokemon, storing each berry separately (splitting up stacks) +export function getPartyBerries(): PokemonItemMap[] { + const pokemonItems: PokemonItemMap[] = []; + globalScene.getPlayerParty().forEach(pokemon => { + const berries = pokemon.getHeldItems().filter(item => isItemInCategory(item, HeldItemCategoryId.BERRY)); + berries.forEach(berryId => { + const berryStack = pokemon.heldItemManager.getStack(berryId); + for (let i = 1; i <= berryStack; i++) { + pokemonItems.push({ item: { id: berryId, stack: 1 }, pokemonId: pokemon.id }); + } + }); + }); + return pokemonItems; +} diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 53088043777..92628b46314 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -1,7 +1,6 @@ import { globalScene } from "#app/global-scene"; import i18next from "i18next"; import { isNullOrUndefined, randSeedInt } from "#app/utils/common"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { @@ -28,8 +27,6 @@ import { showEncounterText, } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/data/data-lists"; import { Gender } from "#app/data/gender"; import type { PermanentStat } from "#enums/stat"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; @@ -37,6 +34,7 @@ import { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; import type { AbilityId } from "#enums/ability-id"; import type { PokeballType } from "#enums/pokeball"; import { StatusEffect } from "#enums/status-effect"; +import type { HeldItemId } from "#enums/held-item-id"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; @@ -369,60 +367,13 @@ export function applyHealToPokemon(pokemon: PlayerPokemon, heal: number) { applyHpChangeToPokemon(pokemon, heal); } -/** - * Will modify all of a Pokemon's base stats by a flat value - * Base stats can never go below 1 - * @param pokemon - * @param value - */ -export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, good: boolean) { - const modType = modifierTypes - .MYSTERY_ENCOUNTER_SHUCKLE_JUICE() - .generateType(globalScene.getPlayerParty(), [good ? 10 : -15]) - ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); - const modifier = modType?.newModifier(pokemon); - if (modifier) { - globalScene.addModifier(modifier, false, false, false, true); - pokemon.calculateStats(); +export function applyHeldItemWithFallback(pokemon: Pokemon, item: HeldItemId, fallbackItem?: HeldItemId) { + const added = pokemon.heldItemManager.add(item); + if (!added && fallbackItem) { + pokemon.heldItemManager.add(fallbackItem); } } -/** - * Will attempt to add a new modifier to a Pokemon. - * If the Pokemon already has max stacks of that item, it will instead apply 'fallbackModifierType', if specified. - * @param scene - * @param pokemon - * @param modType - * @param fallbackModifierType - */ -export async function applyModifierTypeToPlayerPokemon( - pokemon: PlayerPokemon, - modType: PokemonHeldItemModifierType, - fallbackModifierType?: PokemonHeldItemModifierType, -) { - // Check if the Pokemon has max stacks of that item already - const modifier = modType.newModifier(pokemon); - const existing = globalScene.findModifier( - m => - m instanceof PokemonHeldItemModifier && - m.type.id === modType.id && - m.pokemonId === pokemon.id && - m.matchType(modifier), - ) as PokemonHeldItemModifier; - - // At max stacks - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { - if (!fallbackModifierType) { - return; - } - - // Apply fallback - return applyModifierTypeToPlayerPokemon(pokemon, fallbackModifierType); - } - - globalScene.addModifier(modifier, false, false, false, true); -} - /** * Alternative to using AttemptCapturePhase * Assumes player sprite is visible on the screen (this is intended for non-combat uses) @@ -687,20 +638,10 @@ export async function catchPokemon( } }; const addToParty = (slotIndex?: number) => { - const newPokemon = pokemon.addToParty(pokeballType, slotIndex); - const modifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); + pokemon.addToParty(pokeballType, slotIndex); if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === 6) { globalScene.validateAchv(achvs.SHINY_PARTY); } - Promise.all(modifiers.map(m => globalScene.addModifier(m, true))).then(() => { - globalScene.updateModifiers(true); - removePokemon(); - if (newPokemon) { - newPokemon.loadAssets().then(end); - } else { - end(); - } - }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { if (globalScene.getPlayerParty().length === 6) { @@ -725,6 +666,7 @@ export async function catchPokemon( pokemon.variant, pokemon.ivs, pokemon.nature, + pokemon.heldItemManager.generateHeldItemConfiguration(), pokemon, ); globalScene.ui.setMode( diff --git a/src/data/pokeball.ts b/src/data/pokeball.ts index a479fd8068a..b39b29561b4 100644 --- a/src/data/pokeball.ts +++ b/src/data/pokeball.ts @@ -1,4 +1,5 @@ import { globalScene } from "#app/global-scene"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; import { NumberHolder } from "#app/utils/common"; import { PokeballType } from "#enums/pokeball"; import i18next from "i18next"; @@ -93,7 +94,9 @@ export function getCriticalCaptureChance(modifiedCatchRate: number): number { } const dexCount = globalScene.gameData.getSpeciesCount(d => !!d.caughtAttr); const catchingCharmMultiplier = new NumberHolder(1); - globalScene.findModifier(m => m.is("CriticalCatchChanceBoosterModifier"))?.apply(catchingCharmMultiplier); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.CRITICAL_CATCH_CHANCE_BOOSTER, { + numberHolder: catchingCharmMultiplier, + }); const dexMultiplier = globalScene.gameMode.isDaily || dexCount > 800 ? 2.5 diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index ea129454034..b21bf78a801 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -25,6 +25,11 @@ import { type SpeciesFormChangeTrigger, SpeciesFormChangeWeatherTrigger, } from "./pokemon-forms/form-change-triggers"; +import i18next from "i18next"; + +export function formChangeItemName(id: FormChangeItem) { + return i18next.t(`modifierType:FormChangeItem.${FormChangeItem[id]}`); +} export type SpeciesFormChangeConditionPredicate = (p: Pokemon) => boolean; export type SpeciesFormChangeConditionEnforceFunc = (p: Pokemon) => void; diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index 3726781d9e3..4c3f103f88f 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -3,7 +3,6 @@ import { coerceArray, type Constructor } from "#app/utils/common"; import type { TimeOfDay } from "#enums/time-of-day"; import type Pokemon from "#app/field/pokemon"; import type { SpeciesFormChange } from "#app/data/pokemon-forms"; -import type { PokemonFormChangeItemModifier } from "#app/modifier/modifier"; import { getPokemonNameWithAffix } from "#app/messages"; import { globalScene } from "#app/global-scene"; import { FormChangeItem } from "#enums/form-change-item"; @@ -77,16 +76,12 @@ export class SpeciesFormChangeItemTrigger extends SpeciesFormChangeTrigger { } canChange(pokemon: Pokemon): boolean { - return !!globalScene.findModifier(r => { - // Assume that if m has the `formChangeItem` property, then it is a PokemonFormChangeItemModifier - const m = r as PokemonFormChangeItemModifier; - return ( - "formChangeItem" in m && - m.pokemonId === pokemon.id && - m.formChangeItem === this.item && - m.active === this.active - ); - }); + const matchItem = pokemon.heldItemManager.formChangeItems[this.item]; + if (!matchItem) { + return false; + } + console.log("CAN CHANGE FORMS:", matchItem.active === this.active); + return matchItem.active === this.active; } } diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 7c8b5b29fcd..0625ded7df0 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -49,7 +49,7 @@ import type { EnemyPokemon } from "#app/field/pokemon"; import type { EvilTeam } from "./evil-admin-trainer-pools"; import type { PartyMemberFunc, - GenModifiersFunc, + GenTrainerItemsFunc, GenAIFunc, PartyTemplateFunc, TrainerTierPools, @@ -118,7 +118,7 @@ export class TrainerConfig { public femaleEncounterBgm: string; public doubleEncounterBgm: string; public victoryBgm: string; - public genModifiersFunc: GenModifiersFunc; + public genModifiersFunc: GenTrainerItemsFunc; public genAIFuncs: GenAIFunc[] = []; public modifierRewardFuncs: ModifierTypeFunc[] = []; public partyTemplates: TrainerPartyTemplate[]; @@ -470,7 +470,7 @@ export class TrainerConfig { return this; } - setGenModifiersFunc(genModifiersFunc: GenModifiersFunc): TrainerConfig { + setGenModifiersFunc(genModifiersFunc: GenTrainerItemsFunc): TrainerConfig { this.genModifiersFunc = genModifiersFunc; return this; } @@ -1004,6 +1004,7 @@ export function getRandomPartyMemberFunc( undefined, false, undefined, + undefined, postProcess, ); }; @@ -1028,7 +1029,16 @@ function getSpeciesFilterRandomPartyMemberFunc( .getTrainerSpeciesForLevel(level, true, strength, waveIndex), ); - return globalScene.addEnemyPokemon(species, level, trainerSlot, undefined, false, undefined, postProcess); + return globalScene.addEnemyPokemon( + species, + level, + trainerSlot, + undefined, + false, + undefined, + undefined, + postProcess, + ); }; } diff --git a/src/enums/held-item-id.ts b/src/enums/held-item-id.ts new file mode 100644 index 00000000000..944d76b0a0f --- /dev/null +++ b/src/enums/held-item-id.ts @@ -0,0 +1,147 @@ +// TODO: make category the lower 2 bytes +export const HeldItemId = { + NONE: 0x0000, + + // Berries + SITRUS_BERRY: 0x0101, + LUM_BERRY: 0x0102, + ENIGMA_BERRY: 0x0103, + LIECHI_BERRY: 0x0104, + GANLON_BERRY: 0x0105, + PETAYA_BERRY: 0x0106, + APICOT_BERRY: 0x0107, + SALAC_BERRY: 0x0108, + LANSAT_BERRY: 0x0109, + STARF_BERRY: 0x010A, + LEPPA_BERRY: 0x010B, + + // Other items that are consumed + REVIVER_SEED: 0x0201, + WHITE_HERB: 0x0202, + + // Type Boosters + SILK_SCARF: 0x0301, + BLACK_BELT: 0x0302, + SHARP_BEAK: 0x0303, + POISON_BARB: 0x0304, + SOFT_SAND: 0x0305, + HARD_STONE: 0x0306, + SILVER_POWDER: 0x0307, + SPELL_TAG: 0x0308, + METAL_COAT: 0x0309, + CHARCOAL: 0x030A, + MYSTIC_WATER: 0x030B, + MIRACLE_SEED: 0x030C, + MAGNET: 0x030D, + TWISTED_SPOON: 0x030E, + NEVER_MELT_ICE: 0x030F, + DRAGON_FANG: 0x0310, + BLACK_GLASSES: 0x0311, + FAIRY_FEATHER: 0x0312, + + // Species Stat Boosters + LIGHT_BALL: 0x0401, + THICK_CLUB: 0x0402, + METAL_POWDER: 0x0403, + QUICK_POWDER: 0x0404, + DEEP_SEA_SCALE: 0x0405, + DEEP_SEA_TOOTH: 0x0406, + + // Crit Boosters + SCOPE_LENS: 0x0501, + LEEK: 0x0502, + + // Items increasing gains + LUCKY_EGG: 0x0601, + GOLDEN_EGG: 0x0602, + SOOTHE_BELL: 0x0603, + + // Unique items + FOCUS_BAND: 0x0701, + QUICK_CLAW: 0x0702, + KINGS_ROCK: 0x0703, + LEFTOVERS: 0x0704, + SHELL_BELL: 0x0705, + MYSTICAL_ROCK: 0x0706, + WIDE_LENS: 0x0707, + MULTI_LENS: 0x0708, + GOLDEN_PUNCH: 0x0709, + GRIP_CLAW: 0x070A, + TOXIC_ORB: 0x070B, + FLAME_ORB: 0x070C, + SOUL_DEW: 0x070D, + BATON: 0x070E, + MINI_BLACK_HOLE: 0x070F, + EVIOLITE: 0x0710, + + // Vitamins + HP_UP: 0x0801, + PROTEIN: 0x0802, + IRON: 0x0803, + CALCIUM: 0x0804, + ZINC: 0x0805, + CARBOS: 0x0806, + + // Other stat boosting items + SHUCKLE_JUICE_GOOD: 0x0901, + SHUCKLE_JUICE_BAD: 0x0902, + OLD_GATEAU: 0x0903, + MACHO_BRACE: 0x0904, + + // Evo trackers + GIMMIGHOUL_EVO_TRACKER: 0x0A01, +}; + +export type HeldItemId = (typeof HeldItemId)[keyof typeof HeldItemId]; + +type HeldItemName = keyof typeof HeldItemId; +type HeldItemValue = typeof HeldItemId[HeldItemName]; + +// Use a type-safe reducer to force number keys and values +export const HeldItemNames: Record = Object.entries(HeldItemId).reduce( + (acc, [key, value]) => { + acc[value as HeldItemValue] = key as HeldItemName; + return acc; + }, + {} as Record +); + + +export const HeldItemCategoryId = { + NONE: 0x0000, + BERRY: 0x0100, + CONSUMABLE: 0x0200, + TYPE_ATTACK_BOOSTER: 0x0300, + SPECIES_STAT_BOOSTER: 0x0400, + CRIT_BOOSTER: 0x0500, + GAIN_INCREASE: 0x0600, + UNIQUE: 0x0700, + VITAMIN: 0x0800, + BASE_STAT_BOOST: 0x0900, + EVO_TRACKER: 0x0A00, +}; + +export type HeldItemCategoryId = (typeof HeldItemCategoryId)[keyof typeof HeldItemCategoryId]; + +const ITEM_CATEGORY_MASK = 0xFF00 + +export function getHeldItemCategory(itemId: HeldItemId): HeldItemCategoryId { + return itemId & ITEM_CATEGORY_MASK; +} + +export function isCategoryId(categoryId: HeldItemCategoryId): boolean { + return (categoryId & ITEM_CATEGORY_MASK) === categoryId; +} + +export function isItemInCategory(itemId: HeldItemId, category: HeldItemCategoryId): boolean { + return getHeldItemCategory(itemId) === category; +} + +export function isItemInRequested( + itemId: HeldItemId, + requestedItems: (HeldItemCategoryId | HeldItemId)[] +): boolean { + return requestedItems.some(entry => { + itemId === entry || (itemId & ITEM_CATEGORY_MASK) === entry + }); +} diff --git a/src/enums/modifier-pool-type.ts b/src/enums/modifier-pool-type.ts index 0d2b92ba80d..6513dc45ac1 100644 --- a/src/enums/modifier-pool-type.ts +++ b/src/enums/modifier-pool-type.ts @@ -1,7 +1,13 @@ export enum ModifierPoolType { PLAYER, +} + +export enum HeldItemPoolType { WILD, TRAINER, - ENEMY_BUFF, - DAILY_STARTER + DAILY_STARTER, } + +export enum TrainerItemPoolType { + ENEMY_BUFF, +} \ No newline at end of file diff --git a/src/enums/modifier-tier.ts b/src/enums/reward-tier.ts similarity index 68% rename from src/enums/modifier-tier.ts rename to src/enums/reward-tier.ts index d8a75e41b0a..e7ccc1d9166 100644 --- a/src/enums/modifier-tier.ts +++ b/src/enums/reward-tier.ts @@ -1,4 +1,4 @@ -export enum ModifierTier { +export enum RewardTier { COMMON, GREAT, ULTRA, diff --git a/src/enums/trainer-item-id.ts b/src/enums/trainer-item-id.ts new file mode 100644 index 00000000000..8069da2836e --- /dev/null +++ b/src/enums/trainer-item-id.ts @@ -0,0 +1,68 @@ +export const TrainerItemId = { + NONE: 0x0000, + + MAP: 0x0B01, + IV_SCANNER: 0x0B02, + LOCK_CAPSULE: 0x0B03, + MEGA_BRACELET: 0x0B04, + DYNAMAX_BAND: 0x0B05, + TERA_ORB: 0x0B06, + + GOLDEN_POKEBALL: 0x0B07, + + OVAL_CHARM: 0x0B08, + EXP_SHARE: 0x0B09, + EXP_BALANCE: 0x0B0A, + + CANDY_JAR: 0x0B0B, + BERRY_POUCH: 0x0B0C, + + HEALING_CHARM: 0x0B0D, + EXP_CHARM: 0x0B0E, + SUPER_EXP_CHARM: 0x0B0F, + GOLDEN_EXP_CHARM: 0x0B10, + AMULET_COIN: 0x0B11, + + ABILITY_CHARM: 0x0B12, + SHINY_CHARM: 0x0B13, + CATCHING_CHARM: 0x0B14, + + BLACK_SLUDGE: 0x0B15, + GOLDEN_BUG_NET: 0x0B16, + + LURE: 0x0C01, + SUPER_LURE: 0x0C02, + MAX_LURE: 0x0C03, + + X_ATTACK: 0x0D01, + X_DEFENSE: 0x0D02, + X_SP_ATK: 0x0D03, + X_SP_DEF: 0x0D04, + X_SPEED: 0x0D05, + X_ACCURACY: 0x0D06, + DIRE_HIT: 0x0D07, + + ENEMY_DAMAGE_BOOSTER: 0x0E01, + ENEMY_DAMAGE_REDUCTION: 0x0E02, + ENEMY_HEAL: 0x0E03, + ENEMY_ATTACK_POISON_CHANCE: 0x0E04, + ENEMY_ATTACK_PARALYZE_CHANCE: 0x0E05, + ENEMY_ATTACK_BURN_CHANCE: 0x0E06, + ENEMY_STATUS_EFFECT_HEAL_CHANCE: 0x0E07, + ENEMY_ENDURE_CHANCE: 0x0E08, + ENEMY_FUSED_CHANCE: 0x0E09, +}; + +export type TrainerItemId = (typeof TrainerItemId)[keyof typeof TrainerItemId]; + +type TrainerItemName = keyof typeof TrainerItemId; +type TrainerItemValue = typeof TrainerItemId[TrainerItemName]; + +// Use a type-safe reducer to force number keys and values +export const TrainerItemNames: Record = Object.entries(TrainerItemId).reduce( + (acc, [key, value]) => { + acc[value as TrainerItemValue] = key as TrainerItemName; + return acc; + }, + {} as Record +); \ No newline at end of file diff --git a/src/events/battle-scene.ts b/src/events/battle-scene.ts index 83d260bd7d2..7e454d813e2 100644 --- a/src/events/battle-scene.ts +++ b/src/events/battle-scene.ts @@ -1,5 +1,6 @@ +import type Pokemon from "#app/field/pokemon"; +import type { BerryType } from "#enums/berry-type"; import type Move from "../data/moves/move"; -import type { BerryModifier } from "../modifier/modifier"; /** Alias for all {@linkcode BattleScene} events */ export enum BattleSceneEventType { @@ -81,12 +82,13 @@ export class MoveUsedEvent extends Event { * @extends Event */ export class BerryUsedEvent extends Event { - /** The {@linkcode BerryModifier} being used */ - public berryModifier: BerryModifier; - constructor(berry: BerryModifier) { + /** The {@linkcode BerryType} being used */ + public pokemon: Pokemon; + public berryType: BerryType; + constructor(pokemon: Pokemon, berryType: BerryType) { super(BattleSceneEventType.BERRY_USED); - - this.berryModifier = berry; + this.pokemon = pokemon; + this.berryType = berryType; } } diff --git a/src/field/arena.ts b/src/field/arena.ts index c5e7edae4d8..08ed42ff72a 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -34,7 +34,8 @@ import { SpeciesFormChangeWeatherTrigger, } from "#app/data/pokemon-forms/form-change-triggers"; import { WeatherType } from "#enums/weather-type"; -import { FieldEffectModifier } from "#app/modifier/modifier"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; export class Arena { public biomeType: BiomeId; @@ -338,7 +339,7 @@ export class Arena { if (!isNullOrUndefined(user)) { weatherDuration.value = 5; - globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); + applyHeldItems(HELD_ITEM_EFFECT.FIELD_EFFECT, { pokemon: user, fieldDuration: weatherDuration }); } this.weather = weather ? new Weather(weather, weatherDuration.value) : null; @@ -425,7 +426,7 @@ export class Arena { if (!isNullOrUndefined(user)) { terrainDuration.value = 5; - globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); + applyHeldItems(HELD_ITEM_EFFECT.FIELD_EFFECT, { pokemon: user, fieldDuration: terrainDuration }); } this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null; diff --git a/src/field/pokemon-held-item-manager.ts b/src/field/pokemon-held-item-manager.ts new file mode 100644 index 00000000000..dceaa7fae0c --- /dev/null +++ b/src/field/pokemon-held-item-manager.ts @@ -0,0 +1,229 @@ +import { allHeldItems } from "#app/data/data-lists"; +import { isItemInCategory, isItemInRequested, type HeldItemCategoryId, type HeldItemId } from "#app/enums/held-item-id"; +import type { FormChangeItem } from "#enums/form-change-item"; +import { + type HeldItemConfiguration, + isHeldItemSpecs, + type HeldItemDataMap, + type HeldItemSpecs, + type FormChangeItemPropertyMap, + type FormChangeItemSpecs, + type HeldItemSaveData, +} from "#app/items/held-item-data-types"; + +export class PokemonItemManager { + public heldItems: HeldItemDataMap; + public formChangeItems: FormChangeItemPropertyMap; + + constructor() { + this.heldItems = {}; + this.formChangeItems = {}; + } + + getItemSpecs(id: HeldItemId): HeldItemSpecs | undefined { + const item = this.heldItems[id]; + if (item) { + const itemSpecs: HeldItemSpecs = { + ...item, + id, + }; + return itemSpecs; + } + return undefined; + } + + generateHeldItemConfiguration(restrictedIds?: HeldItemId[]): HeldItemConfiguration { + const config: HeldItemConfiguration = []; + for (const [k, item] of Object.entries(this.heldItems)) { + const id = Number(k); + if (item && (!restrictedIds || id in restrictedIds)) { + const specs: HeldItemSpecs = { ...item, id }; + config.push({ entry: specs, count: 1 }); + } + } + for (const [k, item] of Object.entries(this.formChangeItems)) { + const id = Number(k); + const specs: FormChangeItemSpecs = { ...item, id }; + config.push({ entry: specs, count: 1 }); + } + return config; + } + + generateSaveData(): HeldItemSaveData { + const saveData: HeldItemSaveData = []; + for (const [k, item] of Object.entries(this.heldItems)) { + const id = Number(k); + if (item) { + const specs: HeldItemSpecs = { ...item, id }; + saveData.push(specs); + } + } + for (const [k, item] of Object.entries(this.formChangeItems)) { + const id = Number(k); + const specs: FormChangeItemSpecs = { ...item, id }; + saveData.push(specs); + } + return saveData; + } + + getHeldItems(): number[] { + return Object.keys(this.heldItems).map(k => Number(k)); + } + + getTransferableHeldItems(): number[] { + return Object.keys(this.heldItems) + .filter(k => allHeldItems[k].isTransferable) + .map(k => Number(k)); + } + + getStealableHeldItems(): number[] { + return Object.keys(this.heldItems) + .filter(k => allHeldItems[k].isStealable) + .map(k => Number(k)); + } + + getSuppressableHeldItems(): number[] { + return Object.keys(this.heldItems) + .filter(k => allHeldItems[k].isSuppressable) + .map(k => Number(k)); + } + + hasItem(itemType: HeldItemId): boolean { + return itemType in this.heldItems; + } + + hasItemCategory(categoryId: HeldItemCategoryId): boolean { + return Object.keys(this.heldItems).some(id => isItemInCategory(Number(id), categoryId)); + } + + getStack(itemType: HeldItemId): number { + const item = this.heldItems[itemType]; + return item ? item.stack : 0; + } + + // Use for tests if necessary to go over stack limit + setStack(itemType: HeldItemId, stack: number): void { + const item = this.heldItems[itemType]; + if (item) { + item.stack = stack; + } + } + + isMaxStack(itemType: HeldItemId): boolean { + const item = this.heldItems[itemType]; + return item ? item.stack >= allHeldItems[itemType].getMaxStackCount() : false; + } + + overrideItems(newItems: HeldItemDataMap) { + this.heldItems = newItems; + // The following is to allow randomly generated item configs to have stack 0 + for (const [item, properties] of Object.entries(this.heldItems)) { + if (!properties || properties.stack <= 0) { + delete this.heldItems[item]; + } + } + } + + add(itemType: HeldItemId | HeldItemSpecs, addStack = 1): boolean { + if (isHeldItemSpecs(itemType)) { + return this.addItemWithSpecs(itemType); + } + + const maxStack = allHeldItems[itemType].getMaxStackCount(); + const item = this.heldItems[itemType]; + + if (item) { + // TODO: We may want an error message of some kind instead + if (item.stack < maxStack) { + item.stack = Math.min(item.stack + addStack, maxStack); + return true; + } + } else { + this.heldItems[itemType] = { stack: Math.min(addStack, maxStack) }; + return true; + } + return false; + } + + addItemWithSpecs(itemSpecs: HeldItemSpecs): boolean { + const id = itemSpecs.id; + const maxStack = allHeldItems[id].getMaxStackCount(); + const item = this.heldItems[id]; + + const tempStack = item?.stack ?? 0; + + this.heldItems[id] = itemSpecs; + this.heldItems[id].stack = Math.min(itemSpecs.stack + tempStack, maxStack); + + return true; + } + + remove(itemType: HeldItemId, removeStack = 1, all = false) { + const item = this.heldItems[itemType]; + + if (item) { + item.stack -= removeStack; + + if (all || item.stack <= 0) { + delete this.heldItems[itemType]; + } + } + } + + filterRequestedItems(requestedItems: (HeldItemCategoryId | HeldItemId)[], transferableOnly = true, exclude = false) { + const currentItems = transferableOnly ? this.getTransferableHeldItems() : this.getHeldItems(); + return currentItems.filter(it => !exclude && isItemInRequested(it, requestedItems)); + } + + getHeldItemCount(): number { + let total = 0; + for (const properties of Object.values(this.heldItems)) { + total += properties?.stack ?? 0; + } + return total; + } + + addFormChangeItem(id: FormChangeItem) { + if (!(id in this.formChangeItems)) { + this.formChangeItems[id] = { active: false }; + } + } + + addFormChangeItemWithSpecs(item: FormChangeItemSpecs) { + if (!(item.id in this.formChangeItems)) { + this.formChangeItems[item.id] = { active: item.active }; + } + } + + hasFormChangeItem(id: FormChangeItem): boolean { + return id in this.formChangeItems; + } + + hasActiveFormChangeItem(id: FormChangeItem): boolean { + const item = this.formChangeItems[id]; + if (item) { + return item.active; + } + return false; + } + + getFormChangeItems(): FormChangeItem[] { + return Object.keys(this.formChangeItems).map(k => Number(k)); + } + + getActiveFormChangeItems(): FormChangeItem[] { + return this.getFormChangeItems().filter(m => this.formChangeItems[m]?.active); + } + + toggleActive(id: FormChangeItem) { + const item = this.formChangeItems[id]; + if (item) { + item.active = !item.active; + } + } + + clearItems() { + this.heldItems = {}; + this.formChangeItems = {}; + } +} diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0a8e8469115..aca18a02ef8 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -51,27 +51,6 @@ import { BATTLE_STATS, EFFECTIVE_STATS, } from "#enums/stat"; -import { - EnemyDamageBoosterModifier, - EnemyDamageReducerModifier, - EnemyFusionChanceModifier, - HiddenAbilityRateBoosterModifier, - BaseStatModifier, - PokemonFriendshipBoosterModifier, - PokemonHeldItemModifier, - PokemonNatureWeightModifier, - ShinyRateBoosterModifier, - SurviveDamageModifier, - TempStatStageBoosterModifier, - TempCritBoosterModifier, - StatBoosterModifier, - CritBoosterModifier, - PokemonBaseStatFlatModifier, - PokemonBaseStatTotalModifier, - PokemonIncrementingStatModifier, - EvoTrackerModifier, - PokemonMultiHitModifier, -} from "#app/modifier/modifier"; import { PokeballType } from "#enums/pokeball"; import { Gender } from "#app/data/gender"; import { Status, getRandomStatus } from "#app/data/status-effect"; @@ -135,7 +114,7 @@ import type { TrainerSlot } from "#enums/trainer-slot"; import Overrides from "#app/overrides"; import i18next from "i18next"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { applyChallenges } from "#app/data/challenge"; import { ChallengeType } from "#enums/challenge-type"; import { AbilityId } from "#enums/ability-id"; @@ -173,6 +152,10 @@ import { doShinySparkleAnim } from "#app/field/anims"; import { MoveFlags } from "#enums/MoveFlags"; import { timedEventManager } from "#app/global-event-manager"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; +import { PokemonItemManager } from "./pokemon-held-item-manager"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { HeldItemId } from "#enums/held-item-id"; import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode"; import { FieldPosition } from "#enums/field-position"; import { HitResult } from "#enums/hit-result"; @@ -182,6 +165,9 @@ import type { IllusionData } from "#app/@types/illusion-data"; import type { TurnMove } from "#app/@types/turn-move"; import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; +import type { HeldItemConfiguration } from "#app/items/held-item-data-types"; +import { assignItemsFromConfiguration } from "#app/items/held-item-pool"; import { getTerrainBlockMessage } from "#app/data/terrain"; import { LearnMoveSituation } from "#enums/learn-move-situation"; @@ -297,6 +283,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { private shinySparkle: Phaser.GameObjects.Sprite; + public heldItemManager: PokemonItemManager; + // TODO: Rework this eventually constructor( x: number, @@ -310,6 +298,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { variant?: Variant, ivs?: number[], nature?: Nature, + heldItemConfig?: HeldItemConfiguration, dataSource?: Pokemon | PokemonData, ) { super(globalScene, x, y); @@ -339,6 +328,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate); this.levelExp = dataSource?.levelExp || 0; + this.heldItemManager = new PokemonItemManager(); + if (heldItemConfig) { + assignItemsFromConfiguration(heldItemConfig, this); + } + if (dataSource) { this.id = dataSource.id; this.hp = dataSource.hp; @@ -416,7 +410,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (level > 1) { const fused = new BooleanHolder(globalScene.gameMode.isSplicedOnly); if (!fused.value && this.isEnemy() && !this.hasTrainer()) { - globalScene.applyModifier(EnemyFusionChanceModifier, false, fused); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_FUSED_CHANCE, { booleanHolder: fused }); } if (fused.value) { @@ -599,7 +593,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Roll for hidden ability chance, applying any ability charms for enemy mons const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); } // If the roll succeeded and we have one, use HA; otherwise pick a random ability @@ -1119,14 +1115,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.setScale(this.getSpriteScale()); } - getHeldItems(): PokemonHeldItemModifier[] { - if (!globalScene) { - return []; - } - return globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, - this.isPlayer(), - ) as PokemonHeldItemModifier[]; + getHeldItems(): HeldItemId[] { + return this.heldItemManager.getHeldItems(); } updateScale(): void { @@ -1356,8 +1346,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getCritStage(source: Pokemon, move: Move): number { const critStage = new NumberHolder(0); applyMoveAttrs("HighCritAttr", source, this, move, critStage); - globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage); - globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); + applyHeldItems(HELD_ITEM_EFFECT.CRIT_BOOST, { pokemon: source, critStage: critStage }); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.TEMP_CRIT_BOOSTER, { numberHolder: critStage }); applyAbAttrs("BonusCritAbAttr", { pokemon: source, critStage }); const critBoostTag = source.getTag(CritBoostTag); if (critBoostTag) { @@ -1411,7 +1401,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ): number { const statVal = new NumberHolder(this.getStat(stat, false)); if (!ignoreHeldItems) { - globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statVal); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: this, stat: stat, statValue: statVal }); } // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway @@ -1519,7 +1509,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const statHolder = new NumberHolder(Math.floor((2 * baseStats[s] + this.ivs[s]) * this.level * 0.01)); if (s === Stat.HP) { statHolder.value = statHolder.value + this.level + 10; - globalScene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, s, statHolder); + applyHeldItems(HELD_ITEM_EFFECT.INCREMENTING_STAT, { pokemon: this, stat: s, statHolder: statHolder }); if (this.hasAbility(AbilityId.WONDER_GUARD, false, true)) { statHolder.value = 1; } @@ -1534,14 +1524,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } else { statHolder.value += 5; const natureStatMultiplier = new NumberHolder(getNatureStatMultiplier(this.getNature(), s)); - globalScene.applyModifier(PokemonNatureWeightModifier, this.isPlayer(), this, natureStatMultiplier); + applyHeldItems(HELD_ITEM_EFFECT.NATURE_WEIGHT_BOOSTER, { pokemon: this, multiplier: natureStatMultiplier }); if (natureStatMultiplier.value !== 1) { statHolder.value = Math.max( Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](statHolder.value * natureStatMultiplier.value), 1, ); } - globalScene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, s, statHolder); + applyHeldItems(HELD_ITEM_EFFECT.INCREMENTING_STAT, { pokemon: this, stat: s, statHolder: statHolder }); } statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, Number.MAX_SAFE_INTEGER); @@ -1554,9 +1544,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const baseStats = this.getSpeciesForm(true).baseStats.slice(0); applyChallenges(ChallengeType.FLIP_STAT, this, baseStats); // Shuckle Juice - globalScene.applyModifiers(PokemonBaseStatTotalModifier, this.isPlayer(), this, baseStats); + applyHeldItems(HELD_ITEM_EFFECT.BASE_STAT_TOTAL, { pokemon: this, baseStats: baseStats }); // Old Gateau - globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); + applyHeldItems(HELD_ITEM_EFFECT.BASE_STAT_FLAT, { pokemon: this, baseStats: baseStats }); if (this.isFusion()) { const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; applyChallenges(ChallengeType.FLIP_STAT, this, fusionBaseStats); @@ -1570,7 +1560,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } // Vitamins - globalScene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); + applyHeldItems(HELD_ITEM_EFFECT.BASE_STAT_BOOSTER, { pokemon: this, baseStats: baseStats }); return baseStats; } @@ -2180,8 +2170,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first - * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) + * Gets the weight of the Pokemon with subtractive abilities (Autotomize) happening first + * and then multiplicative abilities happening after (Heavy Metal and Light Metal) * @returns the kg of the Pokemon (minimum of 0.1) */ public getWeight(): number { @@ -2794,7 +2784,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } if (!this.hasTrainer()) { - globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold }); } } else { shinyThreshold.value = thresholdOverride; @@ -2826,7 +2816,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (timedEventManager.isEventActive()) { shinyThreshold.value *= timedEventManager.getShinyMultiplier(); } - globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold }); } this.shiny = randSeedInt(65536) < shinyThreshold.value; @@ -2898,7 +2888,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const haThreshold = new NumberHolder(thresholdOverride ?? BASE_HIDDEN_ABILITY_CHANCE); if (applyModifiersToOverride) { if (!this.hasTrainer()) { - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, haThreshold); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, { numberHolder: haThreshold }); } } @@ -2912,7 +2902,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public generateFusionSpecies(forStarter?: boolean): void { const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); } const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); @@ -3040,11 +3032,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { - if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) { + if (tmPoolTiers[moveId] === RewardTier.COMMON && this.level >= 15) { movePool.push([moveId, 4]); - } else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) { + } else if (tmPoolTiers[moveId] === RewardTier.GREAT && this.level >= 30) { movePool.push([moveId, 8]); - } else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) { + } else if (tmPoolTiers[moveId] === RewardTier.ULTRA && this.level >= 50) { movePool.push([moveId, 14]); } } @@ -3426,7 +3418,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!ignoreStatStage.value) { const statStageMultiplier = new NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value)); if (!ignoreHeldItems) { - globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.TEMP_STAT_STAGE_BOOSTER, { + numberHolder: statStageMultiplier, + }); } return Math.min(statStageMultiplier.value, 4); } @@ -3460,7 +3454,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: this, stat: Stat.EVA, ignored: ignoreEvaStatStage }); applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage); - globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.TEMP_ACCURACY_BOOSTER, { numberHolder: userAccStage }); userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6); targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value; @@ -3713,14 +3707,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs("FixedDamageAttr", source, this, move, fixedDamage); if (fixedDamage.value) { const multiLensMultiplier = new NumberHolder(1); - globalScene.applyModifiers( - PokemonMultiHitModifier, - source.isPlayer(), - source, - move.id, - null, - multiLensMultiplier, - ); + applyHeldItems(HELD_ITEM_EFFECT.MULTI_HIT, { + pokemon: source, + moveId: move.id, + damageMultiplier: multiLensMultiplier, + }); fixedDamage.value = toDmgValue(fixedDamage.value * multiLensMultiplier.value); return { @@ -3764,14 +3755,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** Multiplier for moves enhanced by Multi-Lens and/or Parental Bond */ const multiStrikeEnhancementMultiplier = new NumberHolder(1); - globalScene.applyModifiers( - PokemonMultiHitModifier, - source.isPlayer(), - source, - move.id, - null, - multiStrikeEnhancementMultiplier, - ); + applyHeldItems(HELD_ITEM_EFFECT.MULTI_HIT, { + pokemon: source, + moveId: move.id, + damageMultiplier: multiStrikeEnhancementMultiplier, + }); if (!ignoreSourceAbility) { applyAbAttrs("AddSecondStrikeAbAttr", { @@ -3890,10 +3878,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** Apply the enemy's Damage and Resistance tokens */ if (!source.isPlayer()) { - globalScene.applyModifiers(EnemyDamageBoosterModifier, false, damage); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_BOOSTER, { numberHolder: damage }); } if (!this.isPlayer()) { - globalScene.applyModifiers(EnemyDamageReducerModifier, false, damage); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_REDUCER, { numberHolder: damage }); } const abAttrParams: PreAttackModifyDamageAbAttrParams = { @@ -3903,7 +3891,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { simulated, damage, }; - /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ + /** Apply this Pokemon's post-calc defensive attributes (e.g. Fur Coat) */ if (!ignoreAbility) { applyAbAttrs("ReceivedMoveDamageMultiplierAbAttr", abAttrParams); @@ -4005,7 +3993,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { surviveDamage.value = this.lapseTag(BattlerTagType.ENDURE_TOKEN); } if (!surviveDamage.value) { - globalScene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage); + applyHeldItems(HELD_ITEM_EFFECT.SURVIVE_CHANCE, { pokemon: this, surviveDamage: surviveDamage }); } if (surviveDamage.value) { damage = this.hp - 1; @@ -4447,7 +4435,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.setScale(this.getSpriteScale()); this.loadAssets().then(() => { this.calculateStats(); - globalScene.updateModifiers(this.isPlayer(), true); + globalScene.updateItems(this.isPlayer()); Promise.all([this.updateInfo(), globalScene.updateFieldScale()]).then(() => resolve()); }); }); @@ -5555,15 +5543,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param forBattle If `false`, do not trigger in-battle effects (such as Unburden) from losing the item. For example, set this to `false` if the Pokemon is giving away the held item for a Mystery Encounter. Default is `true`. * @returns `true` if the item was removed successfully, `false` otherwise. */ - public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle = true): boolean { - if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) { + public loseHeldItem(heldItemId: HeldItemId, forBattle = true): boolean { + if (!this.heldItemManager.hasItem(heldItemId)) { return false; } - heldItem.stackCount--; - if (heldItem.stackCount <= 0) { - globalScene.removeModifier(heldItem, this.isEnemy()); - } + this.heldItemManager.remove(heldItemId); + if (forBattle) { applyAbAttrs("PostItemLostAbAttr", { pokemon: this }); } @@ -5585,13 +5571,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.turnData.berriesEaten.push(berryType); } - - getPersistentTreasureCount(): number { - return ( - this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length + - globalScene.findModifiers(m => m.is("MoneyMultiplierModifier") || m.is("ExtraModifierModifier")).length - ); - } } export class PlayerPokemon extends Pokemon { @@ -5608,9 +5587,24 @@ export class PlayerPokemon extends Pokemon { variant?: Variant, ivs?: number[], nature?: Nature, + heldItemConfig?: HeldItemConfiguration, dataSource?: Pokemon | PokemonData, ) { - super(106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource); + super( + 106, + 148, + species, + level, + abilityIndex, + formIndex, + gender, + shiny, + variant, + ivs, + nature, + heldItemConfig, + dataSource, + ); if (Overrides.STATUS_OVERRIDE) { this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4); @@ -5754,7 +5748,7 @@ export class PlayerPokemon extends Pokemon { fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null, ].filter(d => !!d); const amount = new NumberHolder(friendship); - globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); + applyHeldItems(HELD_ITEM_EFFECT.FRIENDSHIP_BOOSTER, { pokemon: this, friendship: amount }); const candyFriendshipMultiplier = globalScene.gameMode.isClassic ? timedEventManager.getClassicFriendshipMultiplier() : 1; @@ -5814,6 +5808,7 @@ export class PlayerPokemon extends Pokemon { this.variant, this.ivs, this.nature, + this.heldItemManager.generateHeldItemConfiguration(), this, ); this.fusionSpecies = originalFusionSpecies; @@ -5836,6 +5831,7 @@ export class PlayerPokemon extends Pokemon { this.variant, this.ivs, this.nature, + this.heldItemManager.generateHeldItemConfiguration(), this, ); } @@ -5908,9 +5904,9 @@ export class PlayerPokemon extends Pokemon { }); }; if (preEvolution.speciesId === SpeciesId.GIMMIGHOUL) { - const evotracker = this.getHeldItems().filter(m => m instanceof EvoTrackerModifier)[0] ?? null; + const evotracker = this.heldItemManager.hasItem(HeldItemId.GIMMIGHOUL_EVO_TRACKER); if (evotracker) { - globalScene.removeModifier(evotracker); + this.heldItemManager.remove(HeldItemId.GIMMIGHOUL_EVO_TRACKER, 0, true); } } if (!globalScene.gameMode.isDaily || this.metBiome > -1) { @@ -5963,16 +5959,12 @@ export class PlayerPokemon extends Pokemon { globalScene.getPlayerParty().push(newPokemon); newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies); - const modifiers = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, - true, - ) as PokemonHeldItemModifier[]; - modifiers.forEach(m => { - const clonedModifier = m.clone() as PokemonHeldItemModifier; - clonedModifier.pokemonId = newPokemon.id; - globalScene.addModifier(clonedModifier, true); + //TODO: This currently does not consider any values associated with the items e.g. disabled + const heldItems = this.getHeldItems(); + heldItems.forEach(item => { + newPokemon.heldItemManager.add(item, this.heldItemManager.getStack(item)); }); - globalScene.updateModifiers(true); + globalScene.updateItems(true); } } } @@ -5993,6 +5985,7 @@ export class PlayerPokemon extends Pokemon { this.variant, this.ivs, this.nature, + this.heldItemManager.generateHeldItemConfiguration(), this, ); ret.loadAssets().then(() => resolve(ret)); @@ -6017,7 +6010,7 @@ export class PlayerPokemon extends Pokemon { const updateAndResolve = () => { this.loadAssets().then(() => { this.calculateStats(); - globalScene.updateModifiers(true, true); + globalScene.updateItems(true); this.updateInfo(true).then(() => resolve()); }); }; @@ -6083,15 +6076,11 @@ export class PlayerPokemon extends Pokemon { } // combine the two mons' held items - const fusedPartyMemberHeldModifiers = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, - true, - ) as PokemonHeldItemModifier[]; - for (const modifier of fusedPartyMemberHeldModifiers) { - globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false); + const fusedPartyMemberHeldItems = pokemon.getHeldItems(); + for (const item of fusedPartyMemberHeldItems) { + globalScene.tryTransferHeldItem(item, pokemon, this, false, pokemon.heldItemManager.getStack(item), true, false); } - globalScene.updateModifiers(true, true); - globalScene.removePartyMemberModifiers(fusedPartyMemberIndex); + globalScene.updateItems(true); globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this); pokemon @@ -6139,6 +6128,7 @@ export class EnemyPokemon extends Pokemon { trainerSlot: TrainerSlot, boss: boolean, shinyLock = false, + heldItemConfig?: HeldItemConfiguration, dataSource?: PokemonData, ) { super( @@ -6153,6 +6143,7 @@ export class EnemyPokemon extends Pokemon { !shinyLock && dataSource ? dataSource.variant : undefined, undefined, dataSource ? dataSource.nature : undefined, + heldItemConfig, dataSource, ); @@ -6789,6 +6780,7 @@ export class EnemyPokemon extends Pokemon { this.variant, this.ivs, this.nature, + this.heldItemManager.generateHeldItemConfiguration(), this, ); diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 8ac896e2717..472de052360 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -12,7 +12,6 @@ import { TrainerPoolTier } from "#enums/trainer-pool-tier"; import { TeraAIMode } from "#enums/tera-ai-mode"; import type { EnemyPokemon } from "#app/field/pokemon"; import { randSeedWeightedItem, randSeedItem, randSeedInt } from "#app/utils/common"; -import type { PersistentModifier } from "#app/modifier/modifier"; import { ArenaTrapTag } from "#app/data/arena-tag"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { getIsInitialized, initI18n } from "#app/plugins/i18n"; @@ -22,6 +21,7 @@ import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { signatureSpecies } from "#app/data/balance/signature-species"; import { TrainerVariant } from "#enums/trainer-variant"; +import type { TrainerItemConfiguration } from "#app/items/trainer-item-data-types"; export default class Trainer extends Phaser.GameObjects.Container { public config: TrainerConfig; @@ -647,7 +647,7 @@ export default class Trainer extends Phaser.GameObjects.Container { } } - genModifiers(party: EnemyPokemon[]): PersistentModifier[] { + genTrainerItems(party: EnemyPokemon[]): TrainerItemConfiguration { if (this.config.genModifiersFunc) { return this.config.genModifiersFunc(party); } diff --git a/src/items/all-held-items.ts b/src/items/all-held-items.ts new file mode 100644 index 00000000000..8d6f7b37a4c --- /dev/null +++ b/src/items/all-held-items.ts @@ -0,0 +1,215 @@ +import { allHeldItems } from "#app/data/data-lists"; +import { getEnumValues } from "#app/utils/common"; +import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; +import type { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { Stat, type PermanentStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { HELD_ITEM_EFFECT } from "./held-item"; +import { type ACCURACY_BOOST_PARAMS, AccuracyBoosterHeldItem } from "./held-items/accuracy-booster"; +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 BASE_STAT_FLAT_PARAMS, BaseStatFlatHeldItem } from "./held-items/base-stat-flat"; +import { type BASE_STAT_TOTAL_PARAMS, BaseStatTotalHeldItem } from "./held-items/base-stat-total"; +import { type BATON_PARAMS, BatonHeldItem } from "./held-items/baton"; +import { type BERRY_PARAMS, BerryHeldItem, berryTypeToHeldItem } from "./held-items/berry"; +import { type BYPASS_SPEED_CHANCE_PARAMS, BypassSpeedChanceHeldItem } from "./held-items/bypass-speed-chance"; +import { type CRIT_BOOST_PARAMS, CritBoostHeldItem, SpeciesCritBoostHeldItem } from "./held-items/crit-booster"; +import { type DAMAGE_MONEY_REWARD_PARAMS, DamageMoneyRewardHeldItem } from "./held-items/damage-money-reward"; +import { type EVO_TRACKER_PARAMS, GimmighoulEvoTrackerHeldItem } from "./held-items/evo-tracker"; +import { type EXP_BOOST_PARAMS, ExpBoosterHeldItem } from "./held-items/exp-booster"; +import { type FIELD_EFFECT_PARAMS, FieldEffectHeldItem } from "./held-items/field-effect"; +import { type FLINCH_CHANCE_PARAMS, FlinchChanceHeldItem } from "./held-items/flinch-chance"; +import { type FRIENDSHIP_BOOST_PARAMS, FriendshipBoosterHeldItem } from "./held-items/friendship-booster"; +import { type HIT_HEAL_PARAMS, HitHealHeldItem } from "./held-items/hit-heal"; +import { type INCREMENTING_STAT_PARAMS, IncrementingStatHeldItem } from "./held-items/incrementing-stat"; +import { InstantReviveHeldItem, type INSTANT_REVIVE_PARAMS } from "./held-items/instant-revive"; +import { + ContactItemStealChanceHeldItem, + type ITEM_STEAL_PARAMS, + TurnEndItemStealHeldItem, +} from "./held-items/item-steal"; +import { type MULTI_HIT_PARAMS, MultiHitHeldItem } from "./held-items/multi-hit"; +import { type NATURE_WEIGHT_BOOST_PARAMS, NatureWeightBoosterHeldItem } from "./held-items/nature-weight-booster"; +import { + ResetNegativeStatStageHeldItem, + type RESET_NEGATIVE_STAT_STAGE_PARAMS, +} from "./held-items/reset-negative-stat-stage"; +import { + EvolutionStatBoostHeldItem, + SpeciesStatBoostHeldItem, + type STAT_BOOST_PARAMS, +} from "./held-items/stat-booster"; +import { type SURVIVE_CHANCE_PARAMS, SurviveChanceHeldItem } from "./held-items/survive-chance"; +import { type TURN_END_HEAL_PARAMS, TurnEndHealHeldItem } from "./held-items/turn-end-heal"; +import { type TURN_END_STATUS_PARAMS, TurnEndStatusHeldItem } from "./held-items/turn-end-status"; + +export function initHeldItems() { + 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); + + allHeldItems[HeldItemId.REVIVER_SEED] = new InstantReviveHeldItem(HeldItemId.REVIVER_SEED, 1); + allHeldItems[HeldItemId.WHITE_HERB] = new ResetNegativeStatStageHeldItem(HeldItemId.WHITE_HERB, 2); + + // SILK_SCARF, BLACK_BELT, etc... + for (const [typeKey, heldItemType] of Object.entries(attackTypeToHeldItem)) { + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114957526 + const pokemonType = Number(typeKey) as PokemonType; + allHeldItems[heldItemType] = new AttackTypeBoosterHeldItem(heldItemType, 99, pokemonType, 0.2); + } + + // Items that boost specific stats + allHeldItems[HeldItemId.EVIOLITE] = new EvolutionStatBoostHeldItem( + HeldItemId.EVIOLITE, + 1, + [Stat.DEF, Stat.SPDEF], + 1.5, + ); + allHeldItems[HeldItemId.LIGHT_BALL] = new SpeciesStatBoostHeldItem( + HeldItemId.LIGHT_BALL, + 1, + [Stat.ATK, Stat.SPATK], + 2, + [SpeciesId.PIKACHU], + ); + allHeldItems[HeldItemId.THICK_CLUB] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.ATK], 2, [ + SpeciesId.CUBONE, + SpeciesId.MAROWAK, + SpeciesId.ALOLA_MAROWAK, + ]); + allHeldItems[HeldItemId.METAL_POWDER] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.DEF], 2, [ + SpeciesId.DITTO, + ]); + allHeldItems[HeldItemId.QUICK_POWDER] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPD], 2, [ + SpeciesId.DITTO, + ]); + allHeldItems[HeldItemId.DEEP_SEA_SCALE] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPDEF], 2, [ + SpeciesId.CLAMPERL, + ]); + allHeldItems[HeldItemId.DEEP_SEA_TOOTH] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPATK], 2, [ + SpeciesId.CLAMPERL, + ]); + + // Items that boost the crit rate + allHeldItems[HeldItemId.SCOPE_LENS] = new CritBoostHeldItem(HeldItemId.SCOPE_LENS, 1, 1); + allHeldItems[HeldItemId.LEEK] = new SpeciesCritBoostHeldItem(HeldItemId.LEEK, 1, 2, [ + SpeciesId.FARFETCHD, + SpeciesId.GALAR_FARFETCHD, + SpeciesId.SIRFETCHD, + ]); + + allHeldItems[HeldItemId.LUCKY_EGG] = new ExpBoosterHeldItem(HeldItemId.LUCKY_EGG, 99, 40); + allHeldItems[HeldItemId.GOLDEN_EGG] = new ExpBoosterHeldItem(HeldItemId.GOLDEN_EGG, 99, 100); + allHeldItems[HeldItemId.SOOTHE_BELL] = new FriendshipBoosterHeldItem(HeldItemId.SOOTHE_BELL, 3); + + allHeldItems[HeldItemId.LEFTOVERS] = new TurnEndHealHeldItem(HeldItemId.LEFTOVERS, 4); + allHeldItems[HeldItemId.SHELL_BELL] = new HitHealHeldItem(HeldItemId.SHELL_BELL, 4); + + allHeldItems[HeldItemId.FOCUS_BAND] = new SurviveChanceHeldItem(HeldItemId.FOCUS_BAND, 5); + allHeldItems[HeldItemId.QUICK_CLAW] = new BypassSpeedChanceHeldItem(HeldItemId.QUICK_CLAW, 3); + allHeldItems[HeldItemId.KINGS_ROCK] = new FlinchChanceHeldItem(HeldItemId.KINGS_ROCK, 3, 10); + allHeldItems[HeldItemId.MYSTICAL_ROCK] = new FieldEffectHeldItem(HeldItemId.MYSTICAL_ROCK, 2); + allHeldItems[HeldItemId.SOUL_DEW] = new NatureWeightBoosterHeldItem(HeldItemId.SOUL_DEW, 10); + allHeldItems[HeldItemId.WIDE_LENS] = new AccuracyBoosterHeldItem(HeldItemId.WIDE_LENS, 3, 5); + allHeldItems[HeldItemId.MULTI_LENS] = new MultiHitHeldItem(HeldItemId.MULTI_LENS, 2); + allHeldItems[HeldItemId.GOLDEN_PUNCH] = new DamageMoneyRewardHeldItem(HeldItemId.GOLDEN_PUNCH, 5); + allHeldItems[HeldItemId.BATON] = new BatonHeldItem(HeldItemId.BATON, 1); + allHeldItems[HeldItemId.GRIP_CLAW] = new ContactItemStealChanceHeldItem(HeldItemId.GRIP_CLAW, 5, 10); + allHeldItems[HeldItemId.MINI_BLACK_HOLE] = new TurnEndItemStealHeldItem(HeldItemId.MINI_BLACK_HOLE, 1) + .unstealable() + .untransferable(); + + allHeldItems[HeldItemId.FLAME_ORB] = new TurnEndStatusHeldItem(HeldItemId.FLAME_ORB, 1, StatusEffect.BURN); + allHeldItems[HeldItemId.TOXIC_ORB] = new TurnEndStatusHeldItem(HeldItemId.TOXIC_ORB, 1, StatusEffect.TOXIC); + + // vitamins + for (const [statKey, heldItemType] of Object.entries(permanentStatToHeldItem)) { + const stat = Number(statKey) as PermanentStat; + allHeldItems[heldItemType] = new BaseStatBoosterHeldItem(heldItemType, 30, stat) + .unstealable() + .untransferable() + .unsuppressable(); + } + + allHeldItems[HeldItemId.SHUCKLE_JUICE_GOOD] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE_GOOD, 1, 10) + .unstealable() + .untransferable() + .unsuppressable(); + allHeldItems[HeldItemId.SHUCKLE_JUICE_BAD] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE_BAD, 1, -15) + .unstealable() + .untransferable() + .unsuppressable(); + allHeldItems[HeldItemId.OLD_GATEAU] = new BaseStatFlatHeldItem(HeldItemId.OLD_GATEAU, 1) + .unstealable() + .untransferable() + .unsuppressable(); + allHeldItems[HeldItemId.MACHO_BRACE] = new IncrementingStatHeldItem(HeldItemId.MACHO_BRACE, 50) + .unstealable() + .untransferable() + .unsuppressable(); + allHeldItems[HeldItemId.GIMMIGHOUL_EVO_TRACKER] = new GimmighoulEvoTrackerHeldItem( + HeldItemId.GIMMIGHOUL_EVO_TRACKER, + 999, + SpeciesId.GIMMIGHOUL, + 10, + ); +} + +type APPLY_HELD_ITEMS_PARAMS = { + [HELD_ITEM_EFFECT.ATTACK_TYPE_BOOST]: ATTACK_TYPE_BOOST_PARAMS; + [HELD_ITEM_EFFECT.TURN_END_HEAL]: TURN_END_HEAL_PARAMS; + [HELD_ITEM_EFFECT.HIT_HEAL]: HIT_HEAL_PARAMS; + [HELD_ITEM_EFFECT.RESET_NEGATIVE_STAT_STAGE]: RESET_NEGATIVE_STAT_STAGE_PARAMS; + [HELD_ITEM_EFFECT.EXP_BOOSTER]: EXP_BOOST_PARAMS; + [HELD_ITEM_EFFECT.BERRY]: BERRY_PARAMS; + [HELD_ITEM_EFFECT.BASE_STAT_BOOSTER]: BASE_STAT_BOOSTER_PARAMS; + [HELD_ITEM_EFFECT.INSTANT_REVIVE]: INSTANT_REVIVE_PARAMS; + [HELD_ITEM_EFFECT.STAT_BOOST]: STAT_BOOST_PARAMS; + [HELD_ITEM_EFFECT.CRIT_BOOST]: CRIT_BOOST_PARAMS; + [HELD_ITEM_EFFECT.TURN_END_STATUS]: TURN_END_STATUS_PARAMS; + [HELD_ITEM_EFFECT.SURVIVE_CHANCE]: SURVIVE_CHANCE_PARAMS; + [HELD_ITEM_EFFECT.BYPASS_SPEED_CHANCE]: BYPASS_SPEED_CHANCE_PARAMS; + [HELD_ITEM_EFFECT.FLINCH_CHANCE]: FLINCH_CHANCE_PARAMS; + [HELD_ITEM_EFFECT.FIELD_EFFECT]: FIELD_EFFECT_PARAMS; + [HELD_ITEM_EFFECT.FRIENDSHIP_BOOSTER]: FRIENDSHIP_BOOST_PARAMS; + [HELD_ITEM_EFFECT.NATURE_WEIGHT_BOOSTER]: NATURE_WEIGHT_BOOST_PARAMS; + [HELD_ITEM_EFFECT.ACCURACY_BOOSTER]: ACCURACY_BOOST_PARAMS; + [HELD_ITEM_EFFECT.MULTI_HIT]: MULTI_HIT_PARAMS; + [HELD_ITEM_EFFECT.DAMAGE_MONEY_REWARD]: DAMAGE_MONEY_REWARD_PARAMS; + [HELD_ITEM_EFFECT.BATON]: BATON_PARAMS; + [HELD_ITEM_EFFECT.CONTACT_ITEM_STEAL_CHANCE]: ITEM_STEAL_PARAMS; + [HELD_ITEM_EFFECT.TURN_END_ITEM_STEAL]: ITEM_STEAL_PARAMS; + [HELD_ITEM_EFFECT.BASE_STAT_TOTAL]: BASE_STAT_TOTAL_PARAMS; + [HELD_ITEM_EFFECT.BASE_STAT_FLAT]: BASE_STAT_FLAT_PARAMS; + [HELD_ITEM_EFFECT.INCREMENTING_STAT]: INCREMENTING_STAT_PARAMS; + [HELD_ITEM_EFFECT.EVO_TRACKER]: EVO_TRACKER_PARAMS; +}; + +export function applyHeldItems(effect: T, params: APPLY_HELD_ITEMS_PARAMS[T]) { + const pokemon = params.pokemon; + if (pokemon) { + for (const item of Object.keys(pokemon.heldItemManager.heldItems)) { + if (allHeldItems[item].effects.includes(effect)) { + allHeldItems[item].apply(params); + } + } + } +} diff --git a/src/items/all-trainer-items.ts b/src/items/all-trainer-items.ts new file mode 100644 index 00000000000..cd047a6bfa2 --- /dev/null +++ b/src/items/all-trainer-items.ts @@ -0,0 +1,114 @@ +import { allTrainerItems } from "#app/data/data-lists"; +import { Stat, type TempBattleStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { + CriticalCatchChanceBoosterTrainerItem, + DoubleBattleChanceBoosterTrainerItem, + EnemyAttackStatusEffectChanceTrainerItem, + EnemyDamageBoosterTrainerItem, + EnemyDamageReducerTrainerItem, + EnemyEndureChanceTrainerItem, + EnemyFusionChanceTrainerItem, + EnemyStatusEffectHealChanceTrainerItem, + EnemyTurnHealTrainerItem, + ExpBoosterTrainerItem, + ExtraRewardTrainerItem, + HealingBoosterTrainerItem, + HealShopCostTrainerItem, + HiddenAbilityChanceBoosterTrainerItem, + LevelIncrementBoosterTrainerItem, + MoneyMultiplierTrainerItem, + PreserveBerryTrainerItem, + ShinyRateBoosterTrainerItem, + TempAccuracyBoosterTrainerItem, + TempCritBoosterTrainerItem, + TempStatStageBoosterTrainerItem, + tempStatToTrainerItem, + TrainerItem, +} from "./trainer-item"; + +export function initTrainerItems() { + allTrainerItems[TrainerItemId.MAP] = new TrainerItem(TrainerItemId.MAP, 1); + allTrainerItems[TrainerItemId.IV_SCANNER] = new TrainerItem(TrainerItemId.IV_SCANNER, 1); + allTrainerItems[TrainerItemId.LOCK_CAPSULE] = new TrainerItem(TrainerItemId.LOCK_CAPSULE, 1); + allTrainerItems[TrainerItemId.MEGA_BRACELET] = new TrainerItem(TrainerItemId.MEGA_BRACELET, 1); + allTrainerItems[TrainerItemId.DYNAMAX_BAND] = new TrainerItem(TrainerItemId.DYNAMAX_BAND, 1); + allTrainerItems[TrainerItemId.TERA_ORB] = new TrainerItem(TrainerItemId.TERA_ORB, 1); + + allTrainerItems[TrainerItemId.OVAL_CHARM] = new TrainerItem(TrainerItemId.OVAL_CHARM, 5); + allTrainerItems[TrainerItemId.EXP_SHARE] = new TrainerItem(TrainerItemId.EXP_SHARE, 5); + allTrainerItems[TrainerItemId.EXP_BALANCE] = new TrainerItem(TrainerItemId.EXP_BALANCE, 4); + + allTrainerItems[TrainerItemId.CANDY_JAR] = new LevelIncrementBoosterTrainerItem(TrainerItemId.CANDY_JAR, 99); + allTrainerItems[TrainerItemId.BERRY_POUCH] = new PreserveBerryTrainerItem(TrainerItemId.BERRY_POUCH, 3); + + allTrainerItems[TrainerItemId.HEALING_CHARM] = new HealingBoosterTrainerItem(TrainerItemId.HEALING_CHARM, 1.1, 5); + allTrainerItems[TrainerItemId.EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.EXP_CHARM, 25, 99); + allTrainerItems[TrainerItemId.SUPER_EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.SUPER_EXP_CHARM, 60, 30); + allTrainerItems[TrainerItemId.GOLDEN_EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.GOLDEN_EXP_CHARM, 100, 10); + allTrainerItems[TrainerItemId.AMULET_COIN] = new MoneyMultiplierTrainerItem(TrainerItemId.AMULET_COIN, 5); + + allTrainerItems[TrainerItemId.ABILITY_CHARM] = new HiddenAbilityChanceBoosterTrainerItem( + TrainerItemId.ABILITY_CHARM, + 4, + ); + allTrainerItems[TrainerItemId.GOLDEN_POKEBALL] = new ExtraRewardTrainerItem(TrainerItemId.GOLDEN_POKEBALL, 3); + allTrainerItems[TrainerItemId.SHINY_CHARM] = new ShinyRateBoosterTrainerItem(TrainerItemId.SHINY_CHARM, 4); + allTrainerItems[TrainerItemId.CATCHING_CHARM] = new CriticalCatchChanceBoosterTrainerItem( + TrainerItemId.CATCHING_CHARM, + 3, + ); + + allTrainerItems[TrainerItemId.BLACK_SLUDGE] = new HealShopCostTrainerItem(TrainerItemId.BLACK_SLUDGE, 2.5, 1); + allTrainerItems[TrainerItemId.GOLDEN_BUG_NET] = new TrainerItem(TrainerItemId.GOLDEN_BUG_NET, 1); + + allTrainerItems[TrainerItemId.LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.LURE, 10); + allTrainerItems[TrainerItemId.SUPER_LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.SUPER_LURE, 15); + allTrainerItems[TrainerItemId.MAX_LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.MAX_LURE, 30); + + for (const [statKey, trainerItemType] of Object.entries(tempStatToTrainerItem)) { + const stat = Number(statKey) as TempBattleStat; + if (stat === Stat.ACC) { + allTrainerItems[trainerItemType] = new TempAccuracyBoosterTrainerItem(trainerItemType, 5); + } else { + allTrainerItems[trainerItemType] = new TempStatStageBoosterTrainerItem(trainerItemType, stat, 5); + } + } + allTrainerItems[TrainerItemId.DIRE_HIT] = new TempCritBoosterTrainerItem(TrainerItemId.DIRE_HIT, 5); + + allTrainerItems[TrainerItemId.ENEMY_DAMAGE_BOOSTER] = new EnemyDamageBoosterTrainerItem( + TrainerItemId.ENEMY_DAMAGE_BOOSTER, + ); + allTrainerItems[TrainerItemId.ENEMY_DAMAGE_REDUCTION] = new EnemyDamageReducerTrainerItem( + TrainerItemId.ENEMY_DAMAGE_REDUCTION, + ); + allTrainerItems[TrainerItemId.ENEMY_HEAL] = new EnemyTurnHealTrainerItem(TrainerItemId.ENEMY_HEAL, 10); + allTrainerItems[TrainerItemId.ENEMY_ATTACK_POISON_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem( + TrainerItemId.ENEMY_ATTACK_POISON_CHANCE, + StatusEffect.POISON, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem( + TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE, + StatusEffect.PARALYSIS, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_ATTACK_BURN_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem( + TrainerItemId.ENEMY_ATTACK_BURN_CHANCE, + StatusEffect.BURN, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE] = new EnemyStatusEffectHealChanceTrainerItem( + TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_ENDURE_CHANCE] = new EnemyEndureChanceTrainerItem( + TrainerItemId.ENEMY_ENDURE_CHANCE, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_FUSED_CHANCE] = new EnemyFusionChanceTrainerItem( + TrainerItemId.ENEMY_FUSED_CHANCE, + 10, + ); +} diff --git a/src/items/apply-trainer-items.ts b/src/items/apply-trainer-items.ts new file mode 100644 index 00000000000..f6db615cb25 --- /dev/null +++ b/src/items/apply-trainer-items.ts @@ -0,0 +1,47 @@ +import { allTrainerItems } from "./all-trainer-items"; +import { + type BOOLEAN_HOLDER_PARAMS, + type NUMBER_HOLDER_PARAMS, + type POKEMON_PARAMS, + type PRESERVE_BERRY_PARAMS, + TRAINER_ITEM_EFFECT, +} from "./trainer-item"; +import type { TrainerItemManager } from "./trainer-item-manager"; + +export type APPLY_TRAINER_ITEMS_PARAMS = { + [TRAINER_ITEM_EFFECT.LEVEL_INCREMENT_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.PRESERVE_BERRY]: PRESERVE_BERRY_PARAMS; + [TRAINER_ITEM_EFFECT.HEALING_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.EXP_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.CRITICAL_CATCH_CHANCE_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.EXTRA_REWARD]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.HEAL_SHOP_COST]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.DOUBLE_BATTLE_CHANCE_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.TEMP_STAT_STAGE_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.TEMP_ACCURACY_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.TEMP_CRIT_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_BOOSTER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_REDUCER]: NUMBER_HOLDER_PARAMS; + [TRAINER_ITEM_EFFECT.ENEMY_HEAL]: POKEMON_PARAMS; + [TRAINER_ITEM_EFFECT.ENEMY_ATTACK_STATUS_CHANCE]: POKEMON_PARAMS; + [TRAINER_ITEM_EFFECT.ENEMY_STATUS_HEAL_CHANCE]: POKEMON_PARAMS; + [TRAINER_ITEM_EFFECT.ENEMY_ENDURE_CHANCE]: POKEMON_PARAMS; + [TRAINER_ITEM_EFFECT.ENEMY_FUSED_CHANCE]: BOOLEAN_HOLDER_PARAMS; +}; + +export function applyTrainerItems( + effect: T, + manager: TrainerItemManager, + params: APPLY_TRAINER_ITEMS_PARAMS[T], +) { + if (manager) { + for (const item of Object.keys(manager.trainerItems)) { + if (allTrainerItems[item].effects.includes(effect)) { + allTrainerItems[item].apply(manager, params); + } + } + } +} diff --git a/src/items/held-item-data-types.ts b/src/items/held-item-data-types.ts new file mode 100644 index 00000000000..1670b2bb6d0 --- /dev/null +++ b/src/items/held-item-data-types.ts @@ -0,0 +1,84 @@ +// TODO: move to `src/@types/` +import type Pokemon from "#app/field/pokemon"; +import type { FormChangeItem } from "#enums/form-change-item"; +import type { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import type { RewardTier } from "#enums/reward-tier"; + +export type HeldItemData = { + stack: number; + disabled?: boolean; + cooldown?: number; +}; + +export type HeldItemDataMap = { + [key in HeldItemId]?: HeldItemData; +}; + +export type HeldItemSpecs = HeldItemData & { + id: HeldItemId; +}; + +export function isHeldItemSpecs(entry: any): entry is HeldItemSpecs { + return typeof entry.id === "number" && "stack" in entry; +} + +// Types used for form change items +interface FormChangeItemData { + active: boolean; +} + +export type FormChangeItemPropertyMap = { + [key in FormChangeItem]?: FormChangeItemData; +}; + +export type FormChangeItemSpecs = FormChangeItemData & { + id: FormChangeItem; +}; + +export function isFormChangeItemSpecs(entry: any): entry is FormChangeItemSpecs { + return typeof entry.id === "number" && "active" in entry; +} + +export type HeldItemWeights = { + [key in HeldItemId]?: number; +}; + +export type HeldItemWeightFunc = (party: Pokemon[]) => number; + +export type HeldItemCategoryEntry = HeldItemData & { + id: HeldItemCategoryId; + customWeights?: HeldItemWeights; +}; + +export function isHeldItemCategoryEntry(entry: any): entry is HeldItemCategoryEntry { + return entry?.id && isHeldItemCategoryEntry(entry.id) && "customWeights" in entry; +} + +type HeldItemPoolEntry = { + entry: HeldItemId | HeldItemCategoryId | HeldItemCategoryEntry | HeldItemSpecs; + weight: number | HeldItemWeightFunc; +}; + +export type HeldItemPool = HeldItemPoolEntry[]; + +export function isHeldItemPool(value: any): value is HeldItemPool { + return Array.isArray(value) && value.every(entry => "entry" in entry && "weight" in entry); +} + +export type HeldItemTieredPool = { + [key in RewardTier]?: HeldItemPool; +}; + +type HeldItemConfigurationEntry = { + entry: HeldItemId | HeldItemCategoryId | HeldItemCategoryEntry | HeldItemSpecs | HeldItemPool | FormChangeItemSpecs; + count?: number | (() => number); +}; + +export type HeldItemConfiguration = HeldItemConfigurationEntry[]; + +export type PokemonItemMap = { + item: HeldItemSpecs | FormChangeItemSpecs; + pokemonId: number; +}; + +export type HeldItemSaveData = (HeldItemSpecs | FormChangeItemSpecs)[]; diff --git a/src/items/held-item-pool.ts b/src/items/held-item-pool.ts new file mode 100644 index 00000000000..1cdd95ae744 --- /dev/null +++ b/src/items/held-item-pool.ts @@ -0,0 +1,324 @@ +import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import type Pokemon from "#app/field/pokemon"; +import { coerceArray, getEnumValues, isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#app/utils/common"; +import { BerryType } from "#enums/berry-type"; +import { HeldItemCategoryId, HeldItemId, HeldItemNames, isCategoryId } from "#enums/held-item-id"; +import { HeldItemPoolType } from "#enums/modifier-pool-type"; +import type { PokemonType } from "#enums/pokemon-type"; +import { RewardTier } from "#enums/reward-tier"; +import { PERMANENT_STATS } from "#enums/stat"; +import { allHeldItems } from "./all-held-items"; +import { + type HeldItemConfiguration, + type HeldItemPool, + type HeldItemSaveData, + type HeldItemSpecs, + type HeldItemTieredPool, + type HeldItemWeights, + isFormChangeItemSpecs, + isHeldItemCategoryEntry, + isHeldItemPool, + isHeldItemSpecs, +} from "./held-item-data-types"; +import { attackTypeToHeldItem } from "./held-items/attack-type-booster"; +import { permanentStatToHeldItem } from "./held-items/base-stat-booster"; +import { berryTypeToHeldItem } from "./held-items/berry"; + +export const wildHeldItemPool: HeldItemTieredPool = {}; + +export const trainerHeldItemPool: HeldItemTieredPool = {}; + +export const dailyStarterHeldItemPool: HeldItemTieredPool = {}; + +export function assignDailyRunStarterHeldItems(party: PlayerPokemon[]) { + for (const p of party) { + for (let m = 0; m < 3; m++) { + const tierValue = randSeedInt(64); + + const tier = getDailyRewardTier(tierValue); + + const item = getNewHeldItemFromPool( + getHeldItemPool(HeldItemPoolType.DAILY_STARTER)[tier] as HeldItemPool, + p, + party, + ); + p.heldItemManager.add(item); + } + } +} + +function getDailyRewardTier(tierValue: number): RewardTier { + if (tierValue > 25) { + return RewardTier.COMMON; + } + if (tierValue > 12) { + return RewardTier.GREAT; + } + if (tierValue > 4) { + return RewardTier.ULTRA; + } + if (tierValue > 0) { + return RewardTier.ROGUE; + } + return RewardTier.MASTER; +} + +function getHeldItemPool(poolType: HeldItemPoolType): HeldItemTieredPool { + let pool: HeldItemTieredPool; + switch (poolType) { + case HeldItemPoolType.WILD: + pool = wildHeldItemPool; + break; + case HeldItemPoolType.TRAINER: + pool = trainerHeldItemPool; + break; + case HeldItemPoolType.DAILY_STARTER: + pool = dailyStarterHeldItemPool; + break; + } + return pool; +} + +// TODO: Add proper documentation to this function (once it fully works...) +export function assignEnemyHeldItemsForWave( + waveIndex: number, + count: number, + enemy: EnemyPokemon, + poolType: HeldItemPoolType.WILD | HeldItemPoolType.TRAINER, + upgradeChance = 0, +): void { + for (let i = 1; i <= count; i++) { + const item = getNewHeldItemFromTieredPool( + getHeldItemPool(poolType), + enemy, + upgradeChance && !randSeedInt(upgradeChance) ? 1 : 0, + ); + if (item) { + enemy.heldItemManager.add(item); + } + } + if (!(waveIndex % 1000)) { + enemy.heldItemManager.add(HeldItemId.MINI_BLACK_HOLE); + } +} + +function getRandomTier(): RewardTier { + const tierValue = randSeedInt(1024); + + if (tierValue > 255) { + return RewardTier.COMMON; + } + if (tierValue > 60) { + return RewardTier.GREAT; + } + if (tierValue > 12) { + return RewardTier.ULTRA; + } + if (tierValue) { + return RewardTier.ROGUE; + } + return RewardTier.MASTER; +} + +function determineItemPoolTier(pool: HeldItemTieredPool, upgradeCount?: number): RewardTier { + let tier = getRandomTier(); + + if (!upgradeCount) { + upgradeCount = 0; + } + + tier += upgradeCount; + while (tier && !pool[tier]?.length) { + tier--; + if (upgradeCount) { + upgradeCount--; + } + } + + return tier; +} + +function getNewHeldItemFromTieredPool( + pool: HeldItemTieredPool, + pokemon: Pokemon, + upgradeCount: number, +): HeldItemId | HeldItemSpecs { + const tier = determineItemPoolTier(pool, upgradeCount); + const tierPool = pool[tier]; + + return getNewHeldItemFromPool(tierPool!, pokemon); +} + +export function getNewVitaminHeldItem(customWeights: HeldItemWeights = {}, target?: Pokemon): HeldItemId { + const items = PERMANENT_STATS.map(s => permanentStatToHeldItem[s]); + const weights = items.map(t => (target?.heldItemManager.isMaxStack(t) ? 0 : (customWeights[t] ?? 1))); + const pickedIndex = pickWeightedIndex(weights); + return !isNullOrUndefined(pickedIndex) ? items[pickedIndex] : 0; +} + +export function getNewBerryHeldItem(customWeights: HeldItemWeights = {}, target?: Pokemon): HeldItemId { + const berryTypes = getEnumValues(BerryType); + const items = berryTypes.map(b => berryTypeToHeldItem[b]); + + const weights = items.map(t => + target?.heldItemManager.isMaxStack(t) + ? 0 + : (customWeights[t] ?? + (t === HeldItemId.SITRUS_BERRY || t === HeldItemId.LUM_BERRY || t === HeldItemId.LEPPA_BERRY)) + ? 2 + : 1, + ); + + const pickedIndex = pickWeightedIndex(weights); + return !isNullOrUndefined(pickedIndex) ? items[pickedIndex] : 0; +} + +export function getNewAttackTypeBoosterHeldItem( + pokemon: Pokemon | Pokemon[], + customWeights: HeldItemWeights = {}, + target?: Pokemon, +): HeldItemId | null { + const party = coerceArray(pokemon); + + // TODO: make this consider moves or abilities that change types + const attackMoveTypes = party.flatMap(p => + p + .getMoveset() + .filter(m => m.getMove().is("AttackMove")) + .map(m => p.getMoveType(m.getMove(), true)), + ); + if (!attackMoveTypes.length) { + return null; + } + + const attackMoveTypeWeights = attackMoveTypes.reduce((map, type) => { + const current = map.get(type) ?? 0; + if (current < 3) { + map.set(type, current + 1); + } + return map; + }, new Map()); + + const types = Array.from(attackMoveTypeWeights.keys()); + + const weights = types.map(type => + target?.heldItemManager.isMaxStack(attackTypeToHeldItem[type]) + ? 0 + : (customWeights[attackTypeToHeldItem[type]] ?? attackMoveTypeWeights.get(type)!), + ); + + const pickedIndex = pickWeightedIndex(weights); + return !isNullOrUndefined(pickedIndex) ? attackTypeToHeldItem[types[pickedIndex]] : 0; +} + +export function getNewHeldItemFromCategory( + id: HeldItemCategoryId, + pokemon: Pokemon | Pokemon[], + customWeights: HeldItemWeights = {}, + target?: Pokemon, +): HeldItemId | null { + if (id === HeldItemCategoryId.BERRY) { + return getNewBerryHeldItem(customWeights, target); + } + if (id === HeldItemCategoryId.VITAMIN) { + return getNewVitaminHeldItem(customWeights, target); + } + if (id === HeldItemCategoryId.TYPE_ATTACK_BOOSTER) { + return getNewAttackTypeBoosterHeldItem(pokemon, customWeights, target); + } + return null; +} + +function getPoolWeights(pool: HeldItemPool, pokemon: Pokemon): number[] { + return pool.map(p => { + let weight = typeof p.weight === "function" ? p.weight(coerceArray(pokemon)) : p.weight; + + if (typeof p.entry === "number" && !isCategoryId(p.entry)) { + const itemId = p.entry as HeldItemId; + console.log("ITEM ID: ", itemId, HeldItemNames[itemId]); + console.log(allHeldItems[itemId]); + + if (pokemon.heldItemManager.getStack(itemId) >= allHeldItems[itemId].getMaxStackCount()) { + weight = 0; + } + } + + return weight; + }); +} + +function getNewHeldItemFromPool(pool: HeldItemPool, pokemon: Pokemon, party?: Pokemon[]): HeldItemId | HeldItemSpecs { + const weights = getPoolWeights(pool, pokemon); + + const pickedIndex = pickWeightedIndex(weights); + if (isNullOrUndefined(pickedIndex)) { + return 0; + } + const entry = pool[pickedIndex].entry; + + if (typeof entry === "number") { + if (isCategoryId(entry)) { + return getNewHeldItemFromCategory(entry, party ?? pokemon, {}, pokemon) as HeldItemId; + } + return entry as HeldItemId; + } + + if (isHeldItemCategoryEntry(entry)) { + return getNewHeldItemFromCategory(entry.id, party ?? pokemon, entry?.customWeights, pokemon) as HeldItemId; + } + + return entry as HeldItemSpecs; +} + +function assignItemsFromCategory(id: HeldItemCategoryId, pokemon: Pokemon, count: number) { + for (let i = 1; i <= count; i++) { + const newItem = getNewHeldItemFromCategory(id, pokemon, {}, pokemon); + if (newItem) { + pokemon.heldItemManager.add(newItem); + } + } +} + +export function assignItemsFromConfiguration(config: HeldItemConfiguration, pokemon: Pokemon) { + config.forEach(item => { + const { entry, count } = item; + const actualCount = typeof count === "function" ? count() : (count ?? 1); + + if (typeof entry === "number") { + if (isCategoryId(entry)) { + assignItemsFromCategory(entry, pokemon, actualCount); + } + pokemon.heldItemManager.add(entry, actualCount); + } + + if (isHeldItemSpecs(entry)) { + pokemon.heldItemManager.add(entry); + } + + if (isFormChangeItemSpecs(entry)) { + pokemon.heldItemManager.addFormChangeItemWithSpecs(entry); + } + + if (isHeldItemCategoryEntry(entry)) { + assignItemsFromCategory(entry.id, pokemon, actualCount); + } + + if (isHeldItemPool(entry)) { + for (let i = 1; i <= actualCount; i++) { + const newItem = getNewHeldItemFromPool(entry, pokemon); + if (newItem) { + pokemon.heldItemManager.add(newItem); + } + } + } + }); +} + +// TODO: Handle form change items +export function saveDataToConfig(saveData: HeldItemSaveData): HeldItemConfiguration { + const config: HeldItemConfiguration = []; + for (const specs of saveData) { + config.push({ entry: specs, count: 1 }); + } + return config; +} diff --git a/src/items/held-item-tiers.ts b/src/items/held-item-tiers.ts new file mode 100644 index 00000000000..fb340998198 --- /dev/null +++ b/src/items/held-item-tiers.ts @@ -0,0 +1,47 @@ +import { getHeldItemCategory, HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { RewardTier } from "#enums/reward-tier"; + +export const heldItemTiers = { + [HeldItemCategoryId.BERRY]: RewardTier.COMMON, + + [HeldItemCategoryId.BASE_STAT_BOOST]: RewardTier.GREAT, + [HeldItemId.WHITE_HERB]: RewardTier.GREAT, + [HeldItemId.METAL_POWDER]: RewardTier.GREAT, + [HeldItemId.QUICK_POWDER]: RewardTier.GREAT, + [HeldItemId.DEEP_SEA_SCALE]: RewardTier.GREAT, + [HeldItemId.DEEP_SEA_TOOTH]: RewardTier.GREAT, + [HeldItemId.SOOTHE_BELL]: RewardTier.GREAT, + + [HeldItemCategoryId.TYPE_ATTACK_BOOSTER]: RewardTier.ULTRA, + [HeldItemId.REVIVER_SEED]: RewardTier.ULTRA, + [HeldItemId.LIGHT_BALL]: RewardTier.ULTRA, + [HeldItemId.EVIOLITE]: RewardTier.ULTRA, + [HeldItemId.QUICK_CLAW]: RewardTier.ULTRA, + [HeldItemId.MYSTICAL_ROCK]: RewardTier.ULTRA, + [HeldItemId.WIDE_LENS]: RewardTier.ULTRA, + [HeldItemId.GOLDEN_PUNCH]: RewardTier.ULTRA, + [HeldItemId.TOXIC_ORB]: RewardTier.ULTRA, + [HeldItemId.FLAME_ORB]: RewardTier.ULTRA, + [HeldItemId.LUCKY_EGG]: RewardTier.ULTRA, + + [HeldItemId.FOCUS_BAND]: RewardTier.ROGUE, + [HeldItemId.KINGS_ROCK]: RewardTier.ROGUE, + [HeldItemId.LEFTOVERS]: RewardTier.ROGUE, + [HeldItemId.SHELL_BELL]: RewardTier.ROGUE, + [HeldItemId.GRIP_CLAW]: RewardTier.ROGUE, + [HeldItemId.SOUL_DEW]: RewardTier.ROGUE, + [HeldItemId.BATON]: RewardTier.ROGUE, + [HeldItemId.GOLDEN_EGG]: RewardTier.ULTRA, + + [HeldItemId.MINI_BLACK_HOLE]: RewardTier.MASTER, + [HeldItemId.MULTI_LENS]: RewardTier.MASTER, +}; + +export function getHeldItemTier(item: HeldItemId): RewardTier | undefined { + let tier = heldItemTiers[item]; + if (!tier) { + const category = getHeldItemCategory(item); + tier = heldItemTiers[category]; + } + return tier; +} diff --git a/src/items/held-item.ts b/src/items/held-item.ts new file mode 100644 index 00000000000..d317c1ec0fd --- /dev/null +++ b/src/items/held-item.ts @@ -0,0 +1,171 @@ +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { HeldItemNames, type HeldItemId } from "#enums/held-item-id"; +import i18next from "i18next"; + +export const HELD_ITEM_EFFECT = { + ATTACK_TYPE_BOOST: 1, + TURN_END_HEAL: 2, + HIT_HEAL: 3, + RESET_NEGATIVE_STAT_STAGE: 4, + EXP_BOOSTER: 5, + // Should we actually distinguish different berry effects? + BERRY: 6, + BASE_STAT_BOOSTER: 7, + INSTANT_REVIVE: 8, + STAT_BOOST: 9, + CRIT_BOOST: 10, + TURN_END_STATUS: 11, + SURVIVE_CHANCE: 12, + BYPASS_SPEED_CHANCE: 13, + FLINCH_CHANCE: 14, + FIELD_EFFECT: 15, + FRIENDSHIP_BOOSTER: 16, + NATURE_WEIGHT_BOOSTER: 17, + ACCURACY_BOOSTER: 18, + MULTI_HIT: 19, + DAMAGE_MONEY_REWARD: 20, + BATON: 21, + TURN_END_ITEM_STEAL: 22, + CONTACT_ITEM_STEAL_CHANCE: 23, + EVO_TRACKER: 40, + BASE_STAT_TOTAL: 50, + BASE_STAT_FLAT: 51, + INCREMENTING_STAT: 52, +} as const; + +export type HELD_ITEM_EFFECT = (typeof HELD_ITEM_EFFECT)[keyof typeof HELD_ITEM_EFFECT]; + +export class HeldItem { + // public pokemonId: number; + public type: HeldItemId; + public maxStackCount: number; + public isTransferable = true; + public isStealable = true; + public isSuppressable = true; + + //TODO: If this is actually never changed by any subclass, perhaps it should not be here + public soundName = "se/restore"; + + constructor(type: HeldItemId, maxStackCount = 1) { + this.type = type; + this.maxStackCount = maxStackCount; + + this.isTransferable = true; + this.isStealable = true; + this.isSuppressable = true; + } + + get name(): string { + return i18next.t(`modifierType:ModifierType.${HeldItemNames[this.type]}.name`); + } + + get description(): string { + return i18next.t(`modifierType:ModifierType.${HeldItemNames[this.type]}.description`); + } + + get iconName(): string { + return `${HeldItemNames[this.type]?.toLowerCase()}`; + } + + // TODO: Aren't these fine as just properties to set in the subclass definition? + untransferable(): HeldItem { + this.isTransferable = false; + return this; + } + + unstealable(): HeldItem { + this.isStealable = false; + return this; + } + + unsuppressable(): HeldItem { + this.isSuppressable = false; + return this; + } + + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114950716 + getMaxStackCount(): number { + return this.maxStackCount; + } + + createSummaryIcon(pokemon?: Pokemon, overrideStackCount?: number): Phaser.GameObjects.Container { + const stackCount = overrideStackCount ?? (pokemon ? this.getStackCount(pokemon) : 0); + + const container = globalScene.add.container(0, 0); + + const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5); + container.add(item); + + const stackText = this.getIconStackText(stackCount); + if (stackText) { + container.add(stackText); + } + + container.setScale(0.5); + + return container; + } + + createPokemonIcon(pokemon: Pokemon): Phaser.GameObjects.Container { + const container = globalScene.add.container(0, 0); + + const pokemonIcon = globalScene.addPokemonIcon(pokemon, -2, 10, 0, 0.5, undefined, true); + container.add(pokemonIcon); + container.setName(pokemon.id.toString()); + + const item = globalScene.add + .sprite(16, 16, "items") + .setScale(0.5) + .setOrigin(0, 0.5) + .setTexture("items", this.iconName); + container.add(item); + + const stackText = this.getIconStackText(this.getStackCount(pokemon)); + if (stackText) { + container.add(stackText); + } + + return container; + } + + getIconStackText(stackCount: number): Phaser.GameObjects.BitmapText | null { + if (this.getMaxStackCount() === 1) { + return null; + } + + const text = globalScene.add.bitmapText(10, 15, "item-count", stackCount.toString(), 11); + text.letterSpacing = -0.5; + if (stackCount >= this.getMaxStackCount()) { + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114955458 + text.setTint(0xf89890); + } + text.setOrigin(0); + + return text; + } + + getStackCount(pokemon: Pokemon): number { + const stackCount = pokemon.heldItemManager.getStack(this.type); + return stackCount; + } + + getScoreMultiplier(): number { + return 1; + } +} + +export class ConsumableHeldItem extends HeldItem { + // 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.updateItems(isPlayer); + } + if (unburden) { + applyAbAttrs("PostItemLostAbAttr", { pokemon: pokemon }); + } + } +} diff --git a/src/items/held-items/accuracy-booster.ts b/src/items/held-items/accuracy-booster.ts new file mode 100644 index 00000000000..cba45f29c24 --- /dev/null +++ b/src/items/held-items/accuracy-booster.ts @@ -0,0 +1,47 @@ +import type Pokemon from "#app/field/pokemon"; +import type { NumberHolder } from "#app/utils/common"; +import type { HeldItemId } from "#enums/held-item-id"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; + +export interface ACCURACY_BOOST_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + moveAccuracy: NumberHolder; +} + +export class AccuracyBoosterHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.ACCURACY_BOOSTER]; + + private accuracyAmount: number; + + constructor(type: HeldItemId, maxStackCount = 1, accuracy: number) { + super(type, maxStackCount); + this.accuracyAmount = accuracy; + } + + /** + * Checks if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied + * @param pokemon The {@linkcode Pokemon} to apply the move accuracy boost to + * @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost + * @returns `true` if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, moveAccuracy?: NumberHolder): boolean { + // return super.shouldApply(pokemon, moveAccuracy) && !!moveAccuracy; + // } + + /** + * Applies {@linkcode PokemonMoveAccuracyBoosterModifier} + * @param _pokemon The {@linkcode Pokemon} to apply the move accuracy boost to + * @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost + * @returns always `true` + */ + apply(params: ACCURACY_BOOST_PARAMS): boolean { + const pokemon = params.pokemon; + const moveAccuracy = params.moveAccuracy; + const stackCount = pokemon.heldItemManager.getStack(this.type); + moveAccuracy.value = moveAccuracy.value + this.accuracyAmount * stackCount; + + return true; + } +} diff --git a/src/items/held-items/attack-type-booster.ts b/src/items/held-items/attack-type-booster.ts new file mode 100644 index 00000000000..6fd56721d5c --- /dev/null +++ b/src/items/held-items/attack-type-booster.ts @@ -0,0 +1,78 @@ +import { HeldItemNames, HeldItemId } from "#enums/held-item-id"; +import { PokemonType } from "#enums/pokemon-type"; +import i18next from "i18next"; +import type { NumberHolder } from "#app/utils/common"; +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; + +export interface ATTACK_TYPE_BOOST_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The resolved type of the move */ + moveType: PokemonType; + /** Holder for the damage value */ + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2119660807 + movePower: NumberHolder; +} + +interface AttackTypeToHeldItemMap { + [key: number]: HeldItemId; +} + +export const attackTypeToHeldItem: AttackTypeToHeldItemMap = { + [PokemonType.NORMAL]: HeldItemId.SILK_SCARF, + [PokemonType.FIGHTING]: HeldItemId.BLACK_BELT, + [PokemonType.FLYING]: HeldItemId.SHARP_BEAK, + [PokemonType.POISON]: HeldItemId.POISON_BARB, + [PokemonType.GROUND]: HeldItemId.SOFT_SAND, + [PokemonType.ROCK]: HeldItemId.HARD_STONE, + [PokemonType.BUG]: HeldItemId.SILVER_POWDER, + [PokemonType.GHOST]: HeldItemId.SPELL_TAG, + [PokemonType.STEEL]: HeldItemId.METAL_COAT, + [PokemonType.FIRE]: HeldItemId.CHARCOAL, + [PokemonType.WATER]: HeldItemId.MYSTIC_WATER, + [PokemonType.GRASS]: HeldItemId.MIRACLE_SEED, + [PokemonType.ELECTRIC]: HeldItemId.MAGNET, + [PokemonType.PSYCHIC]: HeldItemId.TWISTED_SPOON, + [PokemonType.ICE]: HeldItemId.NEVER_MELT_ICE, + [PokemonType.DRAGON]: HeldItemId.DRAGON_FANG, + [PokemonType.DARK]: HeldItemId.BLACK_GLASSES, + [PokemonType.FAIRY]: HeldItemId.FAIRY_FEATHER, +}; + +export class AttackTypeBoosterHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_HEAL]; + public moveType: PokemonType; + public powerBoost: number; + + // This constructor may need a revision + constructor(type: HeldItemId, maxStackCount = 1, moveType: PokemonType, powerBoost: number) { + super(type, maxStackCount); + this.moveType = moveType; + this.powerBoost = powerBoost; + } + + get name(): string { + return i18next.t(`modifierType:AttackTypeBoosterItem.${HeldItemNames[this.type]?.toLowerCase()}`); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.AttackTypeBoosterModifierType.description", { + moveType: i18next.t(`pokemonInfo:Type.${PokemonType[this.moveType]}`), + }); + } + + get iconName(): string { + return `${HeldItemNames[this.type]?.toLowerCase()}`; + } + + apply(params: ATTACK_TYPE_BOOST_PARAMS): void { + const pokemon = params.pokemon; + const moveType = params.moveType; + const movePower = params.movePower; + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (moveType === this.moveType && movePower.value >= 1) { + movePower.value = Math.floor(movePower.value * (1 + stackCount * this.powerBoost)); + } + } +} 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..e5075c41a15 --- /dev/null +++ b/src/items/held-items/base-stat-booster.ts @@ -0,0 +1,81 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItemId } from "#enums/held-item-id"; +import { getStatKey, type PermanentStat, Stat } from "#enums/stat"; +import i18next from "i18next"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; + +export interface BASE_STAT_BOOSTER_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + baseStats: number[]; +} + +interface PermanentStatToHeldItemMap { + [key: number]: HeldItemId; +} + +export const permanentStatToHeldItem: PermanentStatToHeldItemMap = { + [Stat.HP]: HeldItemId.HP_UP, + [Stat.ATK]: HeldItemId.PROTEIN, + [Stat.DEF]: HeldItemId.IRON, + [Stat.SPATK]: HeldItemId.CALCIUM, + [Stat.SPDEF]: HeldItemId.ZINC, + [Stat.SPD]: HeldItemId.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: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BASE_STAT_BOOSTER]; + public stat: PermanentStat; + + constructor(type: HeldItemId, 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/base-stat-flat.ts b/src/items/held-items/base-stat-flat.ts new file mode 100644 index 00000000000..75c12b1f0f5 --- /dev/null +++ b/src/items/held-items/base-stat-flat.ts @@ -0,0 +1,71 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; +import { Stat } from "#enums/stat"; +import i18next from "i18next"; + +export interface BASE_STAT_FLAT_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + baseStats: number[]; +} + +/** + * Currently used by Old Gateau item + */ +export class BaseStatFlatHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BASE_STAT_FLAT]; + public isTransferable = false; + + get description(): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description"); + } + + /** + * Checks if the {@linkcode PokemonBaseStatFlatModifier} should be applied to the {@linkcode Pokemon}. + * @param pokemon The {@linkcode Pokemon} that holds the item + * @param baseStats The base stats of the {@linkcode Pokemon} + * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { + // return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); + // } + + /** + * Applies the {@linkcode PokemonBaseStatFlatModifier} + * @param _pokemon The {@linkcode Pokemon} that holds the item + * @param baseStats The base stats of the {@linkcode Pokemon} + * @returns always `true` + */ + apply(params: BASE_STAT_FLAT_PARAMS): boolean { + const pokemon = params.pokemon; + const baseStats = params.baseStats; + const stats = this.getStats(pokemon); + const statModifier = 20; + // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats + baseStats.forEach((v, i) => { + if (stats.includes(i)) { + const newVal = Math.floor(v + statModifier); + baseStats[i] = Math.min(Math.max(newVal, 1), 999999); + } + }); + + return true; + } + + /** + * Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef + * @returns Array of 3 {@linkcode Stat}s to boost + */ + getStats(pokemon: Pokemon): Stat[] { + const stats: Stat[] = []; + const baseStats = pokemon.getSpeciesForm().baseStats.slice(0); + // HP or Speed + stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD); + // Attack or SpAtk + stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); + // Def or SpDef + stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); + return stats; + } +} diff --git a/src/items/held-items/base-stat-total.ts b/src/items/held-items/base-stat-total.ts new file mode 100644 index 00000000000..b48ab0f3430 --- /dev/null +++ b/src/items/held-items/base-stat-total.ts @@ -0,0 +1,70 @@ +import type Pokemon from "#app/field/pokemon"; +import i18next from "i18next"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; +import type { HeldItemId } from "#enums/held-item-id"; + +export interface BASE_STAT_TOTAL_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + baseStats: number[]; +} + +/** + * Currently used by Shuckle Juice item + */ +export class BaseStatTotalHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BASE_STAT_TOTAL]; + public isTransferable = false; + public statModifier: number; + + constructor(type: HeldItemId, maxStackCount = 1, statModifier: number) { + super(type, maxStackCount); + this.statModifier = statModifier; + } + + get name(): string { + return this.statModifier > 0 + ? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.name") + : i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.name"); + } + + // TODO: where is this description shown? + get description(): string { + return this.statModifier > 0 + ? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.description") + : i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.description"); + } + + get iconName(): string { + return this.statModifier > 0 ? "berry_juice_good" : "berry_juice_bad"; + } + + /** + * Checks if {@linkcode PokemonBaseStatTotalModifier} 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 PokemonBaseStatTotalModifier} + * @param _pokemon the {@linkcode Pokemon} to be modified + * @param baseStats the base stats of the {@linkcode Pokemon} + * @returns always `true` + */ + apply(params: BASE_STAT_TOTAL_PARAMS): boolean { + const baseStats = params.baseStats; + // Modifies the passed in baseStats[] array + baseStats.forEach((v, i) => { + // HP is affected by half as much as other stats + const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier); + baseStats[i] = Math.min(Math.max(newVal, 1), 999999); + }); + + return true; + } +} diff --git a/src/items/held-items/baton.ts b/src/items/held-items/baton.ts new file mode 100644 index 00000000000..0b36f6c77c7 --- /dev/null +++ b/src/items/held-items/baton.ts @@ -0,0 +1,22 @@ +import type Pokemon from "#app/field/pokemon"; +import type { NumberHolder } from "#app/utils/common"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; + +export interface BATON_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + expAmount: NumberHolder; +} + +export class BatonHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BATON]; + + /** + * Applies {@linkcode SwitchEffectTransferModifier} + * @returns always `true` + */ + apply(): boolean { + return true; + } +} diff --git a/src/items/held-items/berry.ts b/src/items/held-items/berry.ts new file mode 100644 index 00000000000..f93df60c8a5 --- /dev/null +++ b/src/items/held-items/berry.ts @@ -0,0 +1,96 @@ +import { getBerryEffectDescription, getBerryEffectFunc, getBerryName, getBerryPredicate } from "#app/data/berry"; +import { BerryUsedEvent } from "#app/events/battle-scene"; +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { ConsumableHeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { BooleanHolder } from "#app/utils/common"; +import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; +import { TRAINER_ITEM_EFFECT } from "../trainer-item"; + +interface BerryTypeToHeldItemMap { + [key: number]: HeldItemId; +} + +export const berryTypeToHeldItem: BerryTypeToHeldItemMap = { + [BerryType.SITRUS]: HeldItemId.SITRUS_BERRY, + [BerryType.LUM]: HeldItemId.LUM_BERRY, + [BerryType.ENIGMA]: HeldItemId.ENIGMA_BERRY, + [BerryType.LIECHI]: HeldItemId.LIECHI_BERRY, + [BerryType.GANLON]: HeldItemId.GANLON_BERRY, + [BerryType.PETAYA]: HeldItemId.PETAYA_BERRY, + [BerryType.APICOT]: HeldItemId.APICOT_BERRY, + [BerryType.SALAC]: HeldItemId.SALAC_BERRY, + [BerryType.LANSAT]: HeldItemId.LANSAT_BERRY, + [BerryType.STARF]: HeldItemId.STARF_BERRY, + [BerryType.LEPPA]: HeldItemId.LEPPA_BERRY, +}; + +export interface BERRY_PARAMS { + /** The pokemon with the berry */ + pokemon: Pokemon; +} + +// TODO: Maybe split up into subclasses? +export class BerryHeldItem extends ConsumableHeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_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 + */ + shouldApply(pokemon: Pokemon): boolean { + return 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; + + if (!this.shouldApply(pokemon)) { + return false; + } + + const preserve = new BooleanHolder(false); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.PRESERVE_BERRY, { pokemon: pokemon, doPreserve: preserve }); + const consumed = !preserve.value; + + // munch the berry and trigger unburden-like effects + getBerryEffectFunc(this.berryType)(pokemon); + this.consume(pokemon, 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); + + globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(pokemon, this.berryType)); + + return true; + } +} diff --git a/src/items/held-items/bypass-speed-chance.ts b/src/items/held-items/bypass-speed-chance.ts new file mode 100644 index 00000000000..c454463a14e --- /dev/null +++ b/src/items/held-items/bypass-speed-chance.ts @@ -0,0 +1,62 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import type { BooleanHolder } from "#app/utils/common"; +import { globalScene } from "#app/global-scene"; +import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { Command } from "#enums/command"; + +export interface BYPASS_SPEED_CHANCE_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + doBypassSpeed: BooleanHolder; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class BypassSpeedChanceHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BYPASS_SPEED_CHANCE]; + + /** + * Checks if {@linkcode BypassSpeedChanceModifier} should be applied + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed + * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { + // return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; + // } + + /** + * Applies {@linkcode BypassSpeedChanceModifier} + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed + * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied + */ + apply(params: BYPASS_SPEED_CHANCE_PARAMS): boolean { + const pokemon = params.pokemon; + const doBypassSpeed = params.doBypassSpeed; + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < stackCount) { + doBypassSpeed.value = true; + const isCommandFight = + globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; + + if (isCommandFight) { + globalScene.phaseManager.queueMessage( + i18next.t("modifier:bypassSpeedChanceApply", { + pokemonName: getPokemonNameWithAffix(pokemon), + itemName: this.name, + }), + ); + } + return true; + } + + return false; + } +} diff --git a/src/items/held-items/crit-booster.ts b/src/items/held-items/crit-booster.ts new file mode 100644 index 00000000000..cc305b33640 --- /dev/null +++ b/src/items/held-items/crit-booster.ts @@ -0,0 +1,86 @@ +import type Pokemon from "#app/field/pokemon"; +import type { NumberHolder } from "#app/utils/common"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { SpeciesId } from "#enums/species-id"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; + +export interface CRIT_BOOST_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + critStage: NumberHolder; +} + +/** + * Modifier used for held items that apply critical-hit stage boost(s). + * using a multiplier. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class CritBoostHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.CRIT_BOOST]; + + /** The amount of stages by which the held item increases the current critical-hit stage value */ + protected stageIncrement: number; + + constructor(type: HeldItemId, maxStackCount = 1, stageIncrement: number) { + super(type, maxStackCount); + + this.stageIncrement = stageIncrement; + } + + /** + * Increases the current critical-hit stage value by {@linkcode stageIncrement}. + * @param _pokemon {@linkcode Pokemon} N/A + * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level + * @returns always `true` + */ + apply(params: CRIT_BOOST_PARAMS): boolean { + params.critStage.value += this.stageIncrement; + return true; + } +} + +/** + * Modifier used for held items that apply critical-hit stage boost(s) + * if the holder is of a specific {@linkcode SpeciesId}. + * @extends CritBoosterModifier + * @see {@linkcode shouldApply} + */ +export class SpeciesCritBoostHeldItem extends CritBoostHeldItem { + /** The species that the held item's critical-hit stage boost applies to */ + private species: SpeciesId[]; + + constructor(type: HeldItemId, maxStackCount = 1, stageIncrement: number, species: SpeciesId[]) { + super(type, maxStackCount, stageIncrement); + + this.species = species; + } + + /** + * Checks if the holder's {@linkcode SpeciesId} (or its fused species) is listed + * in {@linkcode species}. + * @param pokemon {@linkcode Pokemon} that holds the held item + * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level + * @returns `true` if the critical-hit level can be incremented, false otherwise + */ + // override shouldApply(pokemon: Pokemon, critStage: NumberHolder): boolean { + // return ( + // super.shouldApply(pokemon, critStage) && + // (this.species.includes(pokemon.getSpeciesForm(true).speciesId) || + // (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId))) + // ); + // } + + apply(params: CRIT_BOOST_PARAMS): boolean { + const pokemon = params.pokemon; + const fitsSpecies = + this.species.includes(pokemon.getSpeciesForm(true).speciesId) || + (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId)); + + if (fitsSpecies) { + return super.apply(params); + } + + return false; + } +} diff --git a/src/items/held-items/damage-money-reward.ts b/src/items/held-items/damage-money-reward.ts new file mode 100644 index 00000000000..0e0905d183e --- /dev/null +++ b/src/items/held-items/damage-money-reward.ts @@ -0,0 +1,33 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { NumberHolder } from "#app/utils/common"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; +import { TRAINER_ITEM_EFFECT } from "../trainer-item"; + +export interface DAMAGE_MONEY_REWARD_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + damage: number; +} + +export class DamageMoneyRewardHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.DAMAGE_MONEY_REWARD]; + + /** + * Applies {@linkcode DamageMoneyRewardModifier} + * @param pokemon The {@linkcode Pokemon} attacking + * @param multiplier {@linkcode NumberHolder} holding the multiplier value + * @returns always `true` + */ + apply(params: DAMAGE_MONEY_REWARD_PARAMS): boolean { + const pokemon = params.pokemon; + const damage = params.damage; + const stackCount = pokemon.heldItemManager.getStack(this.type); + const moneyAmount = new NumberHolder(Math.floor(damage * (0.5 * stackCount))); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); + globalScene.addMoney(moneyAmount.value); + + return true; + } +} diff --git a/src/items/held-items/evo-tracker.ts b/src/items/held-items/evo-tracker.ts new file mode 100644 index 00000000000..37020e67fec --- /dev/null +++ b/src/items/held-items/evo-tracker.ts @@ -0,0 +1,57 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { HeldItemId } from "#enums/held-item-id"; +import type { SpeciesId } from "#enums/species-id"; +import i18next from "i18next"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; +import { TrainerItemId } from "#enums/trainer-item-id"; + +export interface EVO_TRACKER_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +export class EvoTrackerHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.EVO_TRACKER]; + + protected species: SpeciesId; + protected required: number; + public isTransferable = false; + + constructor(type: HeldItemId, maxStackCount = 1, species: SpeciesId, required: number) { + super(type, maxStackCount); + this.species = species; + this.required = required; + } + + /** + * Applies the {@linkcode EvoTrackerModifier} + * @returns always `true` + */ + apply(): boolean { + return true; + } +} + +export class GimmighoulEvoTrackerHeldItem extends EvoTrackerHeldItem { + get name(): string { + return i18next.t("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL.name"); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL.description"); + } + + get iconName(): string { + return "relic_gold"; + } + + getStackCount(pokemon: Pokemon): number { + const stackCount = + pokemon.heldItemManager.getStack(this.type) + + pokemon.heldItemManager.getStack(HeldItemId.GOLDEN_PUNCH) + + globalScene.trainerItems.getStack(TrainerItemId.AMULET_COIN) + + globalScene.trainerItems.getStack(TrainerItemId.GOLDEN_POKEBALL); + return stackCount; + } +} diff --git a/src/items/held-items/exp-booster.ts b/src/items/held-items/exp-booster.ts new file mode 100644 index 00000000000..c4dd7ae9773 --- /dev/null +++ b/src/items/held-items/exp-booster.ts @@ -0,0 +1,56 @@ +import type Pokemon from "#app/field/pokemon"; +import type { NumberHolder } from "#app/utils/common"; +import type { HeldItemId } from "#enums/held-item-id"; +import i18next from "i18next"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; + +export interface EXP_BOOST_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + expAmount: NumberHolder; +} + +export class ExpBoosterHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.EXP_BOOSTER]; + private boostPercent: number; + private boostMultiplier: number; + + constructor(type: HeldItemId, maxStackCount = 1, boostPercent: number) { + super(type, maxStackCount); + this.boostPercent = boostPercent; + this.boostMultiplier = boostPercent * 0.01; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.PokemonExpBoosterModifierType.description", { + boostPercent: this.boostPercent, + }); + } + + // TODO: What do we do with this? Need to look up all the shouldApply + /** + * Checks if {@linkcode PokemonExpBoosterModifier} should be applied + * @param pokemon The {@linkcode Pokemon} to apply the exp boost to + * @param boost {@linkcode NumberHolder} holding the exp boost value + * @returns `true` if {@linkcode PokemonExpBoosterModifier} should be applied + */ + // override shouldApply(pokemon: Pokemon, boost: NumberHolder): boolean { + // return super.shouldApply(pokemon, boost) && !!boost; + // } + + /** + * Applies {@linkcode PokemonExpBoosterModifier} + * @param _pokemon The {@linkcode Pokemon} to apply the exp boost to + * @param boost {@linkcode NumberHolder} holding the exp boost value + * @returns always `true` + */ + apply(params: EXP_BOOST_PARAMS): boolean { + const pokemon = params.pokemon; + const expAmount = params.expAmount; + const stackCount = pokemon.heldItemManager.getStack(this.type); + expAmount.value = Math.floor(expAmount.value * (1 + stackCount * this.boostMultiplier)); + + return true; + } +} diff --git a/src/items/held-items/field-effect.ts b/src/items/held-items/field-effect.ts new file mode 100644 index 00000000000..048aa8f38b6 --- /dev/null +++ b/src/items/held-items/field-effect.ts @@ -0,0 +1,33 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import type { NumberHolder } from "#app/utils/common"; + +export interface FIELD_EFFECT_PARAMS { + pokemon: Pokemon; + /** The pokemon with the item */ + fieldDuration: NumberHolder; +} + +/** + * Modifier used for held items, namely Mystical Rock, that extend the + * duration of weather and terrain effects. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class FieldEffectHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.FIELD_EFFECT]; + + /** + * Provides two more turns per stack to any weather or terrain effect caused + * by the holder. + * @param pokemon {@linkcode Pokemon} that holds the held item + * @param fieldDuration {@linkcode NumberHolder} that stores the current field effect duration + * @returns `true` if the field effect extension was applied successfully + */ + apply(params: FIELD_EFFECT_PARAMS): boolean { + const pokemon = params.pokemon; + const stackCount = pokemon.heldItemManager.getStack(this.type); + params.fieldDuration.value += 2 * stackCount; + return true; + } +} diff --git a/src/items/held-items/flinch-chance.ts b/src/items/held-items/flinch-chance.ts new file mode 100644 index 00000000000..ca46ce79528 --- /dev/null +++ b/src/items/held-items/flinch-chance.ts @@ -0,0 +1,57 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import type { BooleanHolder } from "#app/utils/common"; +import type { HeldItemId } from "#enums/held-item-id"; + +export interface FLINCH_CHANCE_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + flinched: BooleanHolder; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class FlinchChanceHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.FLINCH_CHANCE]; + private chance: number; + + constructor(type: HeldItemId, maxStackCount = 1, chance: number) { + super(type, maxStackCount); + + this.chance = chance; // 10 + } + + /** + * Checks if {@linkcode FlinchChanceModifier} should be applied + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param flinched {@linkcode BooleanHolder} that is `true` if the pokemon flinched + * @returns `true` if {@linkcode FlinchChanceModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, flinched?: BooleanHolder): boolean { + // return super.shouldApply(pokemon, flinched) && !!flinched; + // } + + /** + * Applies {@linkcode FlinchChanceModifier} to randomly flinch targets hit. + * @param pokemon - The {@linkcode Pokemon} that holds the item + * @param flinched - A {@linkcode BooleanHolder} holding whether the pokemon has flinched + * @returns `true` if {@linkcode FlinchChanceModifier} was applied successfully + */ + apply(params: FLINCH_CHANCE_PARAMS): boolean { + const pokemon = params.pokemon; + const flinched = params.flinched; + const stackCount = pokemon.heldItemManager.getStack(this.type); + // The check for pokemon.summonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch + // TODO: Since summonData is always defined now, we can probably remove this + if (pokemon.summonData && !flinched.value && pokemon.randBattleSeedInt(100) < stackCount * this.chance) { + flinched.value = true; + return true; + } + + return false; + } +} diff --git a/src/items/held-items/friendship-booster.ts b/src/items/held-items/friendship-booster.ts new file mode 100644 index 00000000000..72a42ea48dc --- /dev/null +++ b/src/items/held-items/friendship-booster.ts @@ -0,0 +1,34 @@ +import type Pokemon from "#app/field/pokemon"; +import type { NumberHolder } from "#app/utils/common"; +import i18next from "i18next"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; + +export interface FRIENDSHIP_BOOST_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + friendship: NumberHolder; +} + +export class FriendshipBoosterHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.FRIENDSHIP_BOOSTER]; + + get description(): string { + return i18next.t("modifierType:ModifierType.PokemonFriendshipBoosterModifierType.description"); + } + + /** + * Applies {@linkcode PokemonFriendshipBoosterModifier} + * @param _pokemon The {@linkcode Pokemon} to apply the friendship boost to + * @param friendship {@linkcode NumberHolder} holding the friendship boost value + * @returns always `true` + */ + apply(params: FRIENDSHIP_BOOST_PARAMS): boolean { + const pokemon = params.pokemon; + const friendship = params.friendship; + const stackCount = pokemon.heldItemManager.getStack(this.type); + friendship.value = Math.floor(friendship.value * (1 + 0.5 * stackCount)); + + return true; + } +} diff --git a/src/items/held-items/hit-heal.ts b/src/items/held-items/hit-heal.ts new file mode 100644 index 00000000000..1432aa3370b --- /dev/null +++ b/src/items/held-items/hit-heal.ts @@ -0,0 +1,53 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import i18next from "i18next"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; +import { toDmgValue } from "#app/utils/common"; +import { getPokemonNameWithAffix } from "#app/messages"; + +export interface HIT_HEAL_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +export class HitHealHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_HEAL]; + + get name(): string { + return i18next.t("modifierType:ModifierType.SHELL_BELL.name"); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.SHELL_BELL.description"); + } + + get iconName(): string { + return "shell_bell"; + } + + /** + * Applies {@linkcode HitHealModifier} + * @param pokemon The {@linkcode Pokemon} that holds the item + * @returns `true` if the {@linkcode Pokemon} was healed + */ + apply(params: HIT_HEAL_PARAMS): boolean { + const pokemon = params.pokemon; + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (pokemon.turnData.totalDamageDealt > 0 && !pokemon.isFullHp()) { + // TODO: this shouldn't be undefined AFAIK + globalScene.unshiftPhase( + new PokemonHealPhase( + pokemon.getBattlerIndex(), + toDmgValue(pokemon.turnData.totalDamageDealt / 8) * stackCount, + i18next.t("modifier:hitHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.name, + }), + true, + ), + ); + } + return true; + } +} diff --git a/src/items/held-items/incrementing-stat.ts b/src/items/held-items/incrementing-stat.ts new file mode 100644 index 00000000000..262d8d00f70 --- /dev/null +++ b/src/items/held-items/incrementing-stat.ts @@ -0,0 +1,71 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; +import { Stat } from "#enums/stat"; +import type { NumberHolder } from "#app/utils/common"; +import i18next from "i18next"; + +export interface INCREMENTING_STAT_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + stat: Stat; + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2135612276 + statHolder: NumberHolder; +} + +/** + * Currently used by Macho Brace item + */ +export class IncrementingStatHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.INCREMENTING_STAT]; + public isTransferable = false; + + /** + * Checks if the {@linkcode PokemonIncrementingStatModifier} should be applied to the {@linkcode Pokemon}. + * @param pokemon The {@linkcode Pokemon} that holds the item + * @param stat The affected {@linkcode Stat} + * @param statHolder The {@linkcode NumberHolder} that holds the stat + * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, stat?: Stat, statHolder?: NumberHolder): boolean { + // return super.shouldApply(pokemon, stat, statHolder) && !!statHolder; + // } + + get name(): string { + return i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_MACHO_BRACE.name") + " (new)"; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_MACHO_BRACE.description"); + } + + /** + * Applies the {@linkcode PokemonIncrementingStatModifier} + * @param _pokemon The {@linkcode Pokemon} that holds the item + * @param stat The affected {@linkcode Stat} + * @param statHolder The {@linkcode NumberHolder} that holds the stat + * @returns always `true` + */ + apply(params: INCREMENTING_STAT_PARAMS): boolean { + const pokemon = params.pokemon; + const stackCount = pokemon.heldItemManager.getStack(this.type); + const statHolder = params.statHolder; + + // Modifies the passed in stat number holder by +2 per stack for HP, +1 per stack for other stats + // If the Macho Brace is at max stacks (50), adds additional 10% to total HP and 5% to other stats + const isHp = params.stat === Stat.HP; + + if (isHp) { + statHolder.value += 2 * stackCount; + if (stackCount === this.maxStackCount) { + statHolder.value = Math.floor(statHolder.value * 1.1); + } + } else { + statHolder.value += stackCount; + if (stackCount === this.maxStackCount) { + statHolder.value = Math.floor(statHolder.value * 1.05); + } + } + + return true; + } +} diff --git a/src/items/held-items/instant-revive.ts b/src/items/held-items/instant-revive.ts new file mode 100644 index 00000000000..40e88c1e5bf --- /dev/null +++ b/src/items/held-items/instant-revive.ts @@ -0,0 +1,68 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import i18next from "i18next"; +import { ConsumableHeldItem, HELD_ITEM_EFFECT } from "../held-item"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; +import { toDmgValue } from "#app/utils/common"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; + +export interface INSTANT_REVIVE_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +/** + * Modifier used for held items, namely White Herb, that restore adverse stat + * stages in battle. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class InstantReviveHeldItem extends ConsumableHeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.INSTANT_REVIVE]; + + get name(): string { + return i18next.t("modifierType:ModifierType.REVIVER_SEED.name"); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.REVIVER_SEED.description"); + } + + get iconName(): string { + return "reviver_seed"; + } + /** + * Goes through the holder's stat stages and, if any are negative, resets that + * stat stage back to 0. + * @param pokemon {@linkcode Pokemon} that holds the item + * @returns `true` if any stat stages were reset, false otherwise + */ + apply(params: INSTANT_REVIVE_PARAMS): boolean { + const pokemon = params.pokemon; + // Restore the Pokemon to half HP + globalScene.phaseManager.unshiftPhase( + new PokemonHealPhase( + pokemon.getBattlerIndex(), + toDmgValue(pokemon.getMaxHp() / 2), + i18next.t("modifier:pokemonInstantReviveApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.name, + }), + false, + false, + true, + ), + ); + + // Remove the Pokemon's FAINT status + pokemon.resetStatus(true, false, true, false); + + // Reapply Commander on the Pokemon's side of the field, if applicable + const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); + for (const p of field) { + applyAbAttrs("CommanderAbAttr", p, null, false); + } + return true; + } +} diff --git a/src/items/held-items/item-steal.ts b/src/items/held-items/item-steal.ts new file mode 100644 index 00000000000..ec78ebdeeb3 --- /dev/null +++ b/src/items/held-items/item-steal.ts @@ -0,0 +1,168 @@ +import Pokemon from "#app/field/pokemon"; +import { randSeedFloat } from "#app/utils/common"; +import type { HeldItemId } from "#enums/held-item-id"; +import i18next from "i18next"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { allHeldItems } from "../all-held-items"; +import { globalScene } from "#app/global-scene"; + +export interface ITEM_STEAL_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The pokemon to steal from (optional) */ + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2135607083 + target?: Pokemon; +} + +// constructor(type: HeldItemId, maxStackCount = 1, boostPercent: number) { + +/** + * Abstract class for held items that steal other Pokemon's items. + * @see {@linkcode TurnEndItemStealHeldItem} + * @see {@linkcode ContactItemStealChanceHeldItem} + */ +export abstract class ItemTransferHeldItem extends HeldItem { + /** + * Steals an item, chosen randomly, from a set of target Pokemon. + * @param pokemon The {@linkcode Pokemon} holding this item + * @param target The {@linkcode Pokemon} to steal from (optional) + * @param _args N/A + * @returns `true` if an item was stolen; false otherwise. + */ + apply(params: ITEM_STEAL_PARAMS): boolean { + const opponents = this.getTargets(params); + + if (!opponents.length) { + return false; + } + + const pokemon = params.pokemon; + //TODO: Simplify this logic here + const targetPokemon = opponents[pokemon.randBattleSeedInt(opponents.length)]; + + const transferredItemCount = this.getTransferredItemCount(params); + if (!transferredItemCount) { + return false; + } + + // TODO: Change this logic to use held items + const transferredModifierTypes: HeldItemId[] = []; + const heldItems = targetPokemon.heldItemManager.getTransferableHeldItems(); + + for (let i = 0; i < transferredItemCount; i++) { + if (!heldItems.length) { + break; + } + const randItemIndex = pokemon.randBattleSeedInt(heldItems.length); + const randItem = heldItems[randItemIndex]; + // TODO: Fix this after updating the various methods in battle-scene.ts + if (globalScene.tryTransferHeldItem(randItem, targetPokemon, pokemon, false)) { + transferredModifierTypes.push(randItem); + heldItems.splice(randItemIndex, 1); + } + } + + for (const mt of transferredModifierTypes) { + globalScene.phaseManager.queueMessage(this.getTransferMessage(params, mt)); + } + + return !!transferredModifierTypes.length; + } + + abstract getTargets(params: ITEM_STEAL_PARAMS): Pokemon[]; + + abstract getTransferredItemCount(params: ITEM_STEAL_PARAMS): number; + + abstract getTransferMessage(params: ITEM_STEAL_PARAMS, itemId: HeldItemId): string; +} + +/** + * Modifier for held items that steal items from the enemy at the end of + * each turn. + * @see {@linkcode modifierTypes[MINI_BLACK_HOLE]} + */ +export class TurnEndItemStealHeldItem extends ItemTransferHeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_ITEM_STEAL]; + isTransferable = true; + + get description(): string { + return i18next.t("modifierType:ModifierType.TurnHeldItemTransferModifierType.description"); + } + + /** + * Determines the targets to transfer items from when this applies. + * @param pokemon the {@linkcode Pokemon} holding this item + * @param _args N/A + * @returns the opponents of the source {@linkcode Pokemon} + */ + getTargets(params: ITEM_STEAL_PARAMS): Pokemon[] { + return params.pokemon instanceof Pokemon ? params.pokemon.getOpponents() : []; + } + + getTransferredItemCount(_params: ITEM_STEAL_PARAMS): number { + return 1; + } + + getTransferMessage(params: ITEM_STEAL_PARAMS, itemId: HeldItemId): string { + return i18next.t("modifier:turnHeldItemTransferApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(params.target), + itemName: allHeldItems[itemId].name, + pokemonName: params.pokemon.getNameToRender(), + typeName: this.name, + }); + } + + setTransferrableFalse(): void { + this.isTransferable = false; + } +} + +/** + * Modifier for held items that add a chance to steal items from the target of a + * successful attack. + * @see {@linkcode modifierTypes[GRIP_CLAW]} + * @see {@linkcode HeldItemTransferModifier} + */ +export class ContactItemStealChanceHeldItem extends ItemTransferHeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.CONTACT_ITEM_STEAL_CHANCE]; + public readonly chancePercent: number; + public readonly chance: number; + + constructor(type: HeldItemId, maxStackCount = 1, chancePercent: number) { + super(type, maxStackCount); + + this.chancePercent = chancePercent; + this.chance = chancePercent / 100; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.ContactHeldItemTransferChanceModifierType.description", { + chancePercent: this.chancePercent, + }); + } + + /** + * Determines the target to steal items from when this applies. + * @param _holderPokemon The {@linkcode Pokemon} holding this item + * @param targetPokemon The {@linkcode Pokemon} the holder is targeting with an attack + * @returns The target {@linkcode Pokemon} as array for further use in `apply` implementations + */ + getTargets(params: ITEM_STEAL_PARAMS): Pokemon[] { + return params.target ? [params.target] : []; + } + + getTransferredItemCount(params: ITEM_STEAL_PARAMS): number { + const stackCount = params.pokemon.heldItemManager.getStack(this.type); + return randSeedFloat() <= this.chance * stackCount ? 1 : 0; + } + + getTransferMessage(params: ITEM_STEAL_PARAMS, itemId: HeldItemId): string { + return i18next.t("modifier:contactHeldItemTransferApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(params.target), + itemName: allHeldItems[itemId].name, + pokemonName: params.pokemon.getNameToRender(), + typeName: this.name, + }); + } +} diff --git a/src/items/held-items/multi-hit.ts b/src/items/held-items/multi-hit.ts new file mode 100644 index 00000000000..5efbdf71386 --- /dev/null +++ b/src/items/held-items/multi-hit.ts @@ -0,0 +1,89 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { isNullOrUndefined, type NumberHolder } from "#app/utils/common"; +import type { MoveId } from "#enums/move-id"; +import { allMoves } from "#app/data/data-lists"; +import i18next from "i18next"; + +export interface MULTI_HIT_PARAMS { + pokemon: Pokemon; + moveId: MoveId; + count?: NumberHolder; + damageMultiplier?: NumberHolder; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class MultiHitHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.MULTI_HIT]; + + get description(): string { + return i18next.t("modifierType:ModifierType.PokemonMultiHitModifierType.description"); + } + + /** + * For each stack, converts 25 percent of attack damage into an additional strike. + * @param pokemon The {@linkcode Pokemon} using the move + * @param moveId The {@linkcode MoveId | identifier} for the move being used + * @param count {@linkcode NumberHolder} holding the move's hit count for this turn + * @param damageMultiplier {@linkcode NumberHolder} holding a damage multiplier applied to a strike of this move + * @returns always `true` + */ + apply(params: MULTI_HIT_PARAMS): boolean { + const pokemon = params.pokemon; + const move = allMoves[params.moveId]; + /** + * The move must meet Parental Bond's restrictions for this item + * to apply. This means + * - Only attacks are boosted + * - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted + * (though Multi-Lens can still affect moves boosted by Parental Bond) + * - Multi-target moves are not boosted *unless* they can only hit a single Pokemon + * - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted + */ + if (!move.canBeMultiStrikeEnhanced(pokemon)) { + return false; + } + + if (!isNullOrUndefined(params.count)) { + return this.applyHitCountBoost(pokemon, params.count); + } + if (!isNullOrUndefined(params.damageMultiplier)) { + return this.applyDamageModifier(pokemon, params.damageMultiplier); + } + + return false; + } + + /** Adds strikes to a move equal to the number of stacked Multi-Lenses */ + private applyHitCountBoost(pokemon: Pokemon, count: NumberHolder): boolean { + const stackCount = pokemon.heldItemManager.getStack(this.type); + count.value += stackCount; + return true; + } + + /** + * If applied to the first hit of a move, sets the damage multiplier + * equal to (1 - the number of stacked Multi-Lenses). + * Additional strikes beyond that are given a 0.25x damage multiplier + */ + private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) { + // Reduce first hit by 25% for each stack count + damageMultiplier.value *= 1 - 0.25 * stackCount; + return true; + } + if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== stackCount + 1) { + // Deal 25% damage for each remaining Multi Lens hit + damageMultiplier.value *= 0.25; + return true; + } + // An extra hit not caused by Multi Lens -- assume it is Parental Bond + return false; + } +} diff --git a/src/items/held-items/nature-weight-booster.ts b/src/items/held-items/nature-weight-booster.ts new file mode 100644 index 00000000000..ae515a96196 --- /dev/null +++ b/src/items/held-items/nature-weight-booster.ts @@ -0,0 +1,32 @@ +import type Pokemon from "#app/field/pokemon"; +import type { NumberHolder } from "#app/utils/common"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; + +export interface NATURE_WEIGHT_BOOST_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + multiplier: NumberHolder; +} + +export class NatureWeightBoosterHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.NATURE_WEIGHT_BOOSTER]; + + /** + * Applies {@linkcode PokemonNatureWeightModifier} + * @param _pokemon The {@linkcode Pokemon} to apply the nature weight to + * @param multiplier {@linkcode NumberHolder} holding the nature weight + * @returns `true` if multiplier was applied + */ + apply(params: NATURE_WEIGHT_BOOST_PARAMS): boolean { + const pokemon = params.pokemon; + const multiplier = params.multiplier; + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (multiplier.value !== 1) { + multiplier.value += 0.1 * stackCount * (multiplier.value > 1 ? 1 : -1); + return true; + } + + return false; + } +} diff --git a/src/items/held-items/reset-negative-stat-stage.ts b/src/items/held-items/reset-negative-stat-stage.ts new file mode 100644 index 00000000000..94bb72c9ed4 --- /dev/null +++ b/src/items/held-items/reset-negative-stat-stage.ts @@ -0,0 +1,66 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { BATTLE_STATS } from "#enums/stat"; +import i18next from "i18next"; +import { ConsumableHeldItem, HELD_ITEM_EFFECT } from "../held-item"; +import { getPokemonNameWithAffix } from "#app/messages"; + +export interface RESET_NEGATIVE_STAT_STAGE_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Whether the move was used by a player pokemon */ + isPlayer: boolean; +} + +/** + * Modifier used for held items, namely White Herb, that restore adverse stat + * stages in battle. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class ResetNegativeStatStageHeldItem extends ConsumableHeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.RESET_NEGATIVE_STAT_STAGE]; + + get name(): string { + return i18next.t("modifierType:ModifierType.WHITE_HERB.name"); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.WHITE_HERB.description"); + } + + get iconName(): string { + return "white_herb"; + } + /** + * Goes through the holder's stat stages and, if any are negative, resets that + * stat stage back to 0. + * @param pokemon {@linkcode Pokemon} that holds the item + * @returns `true` if any stat stages were reset, false otherwise + */ + apply(params: RESET_NEGATIVE_STAT_STAGE_PARAMS): boolean { + const pokemon = params.pokemon; + const isPlayer = params.isPlayer; + let statRestored = false; + + for (const s of BATTLE_STATS) { + if (pokemon.getStatStage(s) < 0) { + pokemon.setStatStage(s, 0); + statRestored = true; + } + } + + if (statRestored) { + globalScene.phaseManager.queueMessage( + i18next.t("modifier:resetNegativeStatStageApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.name, + }), + ); + + this.consume(pokemon, isPlayer, true, false); + } + + return statRestored; + } +} diff --git a/src/items/held-items/stat-booster.ts b/src/items/held-items/stat-booster.ts new file mode 100644 index 00000000000..711b4f276ab --- /dev/null +++ b/src/items/held-items/stat-booster.ts @@ -0,0 +1,191 @@ +import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import type Pokemon from "#app/field/pokemon"; +import type { NumberHolder } from "#app/utils/common"; +import { HeldItemId } from "#enums/held-item-id"; +import type { SpeciesId } from "#enums/species-id"; +import type { Stat } from "#enums/stat"; +import { HeldItem, HELD_ITEM_EFFECT } from "../held-item"; + +export interface STAT_BOOST_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + stat: Stat; + statValue: NumberHolder; +} + +/** + * Modifier used for held items that Applies {@linkcode Stat} boost(s) + * using a multiplier. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class StatBoostHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.STAT_BOOST]; + /** The stats that the held item boosts */ + protected stats: Stat[]; + /** The multiplier used to increase the relevant stat(s) */ + protected multiplier: number; + + constructor(type: HeldItemId, maxStackCount = 1, stats: Stat[], multiplier: number) { + super(type, maxStackCount); + + this.stats = stats; + this.multiplier = multiplier; + } + + /** + * Checks if the incoming stat is listed in {@linkcode stats} + * @param _pokemon the {@linkcode Pokemon} that holds the item + * @param _stat the {@linkcode Stat} to be boosted + * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat + * @returns `true` if the stat could be boosted, false otherwise + */ + // override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { + // return super.shouldApply(pokemon, stat, statValue) && this.stats.includes(stat); + // } + + /** + * Boosts the incoming stat by a {@linkcode multiplier} if the stat is listed + * in {@linkcode stats}. + * @param _pokemon the {@linkcode Pokemon} that holds the item + * @param _stat the {@linkcode Stat} to be boosted + * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat + * @returns `true` if the stat boost applies successfully, false otherwise + * @see shouldApply + */ + apply(params: STAT_BOOST_PARAMS): boolean { + params.statValue.value *= this.multiplier; + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + return 1; + } +} + +/** + * Modifier used for held items, specifically Eviolite, that apply + * {@linkcode Stat} boost(s) using a multiplier if the holder can evolve. + * @extends StatBoosterModifier + * @see {@linkcode apply} + */ +export class EvolutionStatBoostHeldItem extends StatBoostHeldItem { + /** + * Checks if the stat boosts can apply and if the holder is not currently + * Gigantamax'd. + * @param pokemon {@linkcode Pokemon} that holds the held item + * @param stat {@linkcode Stat} The {@linkcode Stat} to be boosted + * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat + * @returns `true` if the stat boosts can be applied, false otherwise + */ + // override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { + // return super.shouldApply(pokemon, stat, statValue) && !pokemon.isMax(); + // } + + /** + * Boosts the incoming stat value by a {@linkcode EvolutionStatBoosterModifier.multiplier} if the holder + * can evolve. Note that, if the holder is a fusion, they will receive + * only half of the boost if either of the fused members are fully + * evolved. However, if they are both unevolved, the full boost + * will apply. + * @param pokemon {@linkcode Pokemon} that holds the item + * @param _stat {@linkcode Stat} The {@linkcode Stat} to be boosted + * @param statValue{@linkcode NumberHolder} that holds the resulting value of the stat + * @returns `true` if the stat boost applies successfully, false otherwise + * @see shouldApply + */ + override apply(params: STAT_BOOST_PARAMS): boolean { + const pokemon = params.pokemon; + const isUnevolved = pokemon.getSpeciesForm(true).speciesId in pokemonEvolutions; + + if (pokemon.isFusion() && pokemon.getFusionSpeciesForm(true).speciesId in pokemonEvolutions !== isUnevolved) { + // Half boost applied if pokemon is fused and either part of fusion is fully evolved + params.statValue.value *= 1 + (this.multiplier - 1) / 2; + return true; + } + if (isUnevolved && !pokemon.isMax()) { + // Full boost applied if holder is unfused and unevolved or, if fused, both parts of fusion are unevolved + return super.apply(params); + } + + return false; + } +} + +export type SpeciesStatBoosterItemId = + | typeof HeldItemId.LIGHT_BALL + | typeof HeldItemId.THICK_CLUB + | typeof HeldItemId.METAL_POWDER + | typeof HeldItemId.QUICK_POWDER + | typeof HeldItemId.DEEP_SEA_SCALE + | typeof HeldItemId.DEEP_SEA_TOOTH; + +export const SPECIES_STAT_BOOSTER_ITEMS: SpeciesStatBoosterItemId[] = [ + HeldItemId.LIGHT_BALL, + HeldItemId.THICK_CLUB, + HeldItemId.METAL_POWDER, + HeldItemId.QUICK_POWDER, + HeldItemId.DEEP_SEA_SCALE, + HeldItemId.DEEP_SEA_TOOTH, +]; + +/** + * Modifier used for held items that Applies {@linkcode Stat} boost(s) using a + * multiplier if the holder is of a specific {@linkcode SpeciesId}. + * @extends StatBoostHeldItem + * @see {@linkcode apply} + */ +export class SpeciesStatBoostHeldItem extends StatBoostHeldItem { + /** The species that the held item's stat boost(s) apply to */ + public species: SpeciesId[]; + + constructor( + type: SpeciesStatBoosterItemId, + maxStackCount = 1, + stats: Stat[], + multiplier: number, + species: SpeciesId[], + ) { + super(type, maxStackCount, stats, multiplier); + this.species = species; + } + + /** + * Checks if the incoming stat is listed in {@linkcode stats} and if the holder's {@linkcode SpeciesId} + * (or its fused species) is listed in {@linkcode species}. + * @param pokemon {@linkcode Pokemon} that holds the item + * @param stat {@linkcode Stat} being checked at the time + * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat + * @returns `true` if the stat could be boosted, false otherwise + */ + // override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { + // return ( + // super.shouldApply(pokemon, stat, statValue) && + // (this.species.includes(pokemon.getSpeciesForm(true).speciesId) || + // (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId))) + // ); + // } + + apply(params: STAT_BOOST_PARAMS): boolean { + const pokemon = params.pokemon; + const fitsSpecies = + this.species.includes(pokemon.getSpeciesForm(true).speciesId) || + (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId)); + + if (fitsSpecies) { + return super.apply(params); + } + + return false; + } + + /** + * Checks if either parameter is included in the corresponding lists + * @param speciesId {@linkcode SpeciesId} being checked + * @param stat {@linkcode Stat} being checked + * @returns `true` if both parameters are in {@linkcode species} and {@linkcode stats} respectively, false otherwise + */ + contains(speciesId: SpeciesId, stat: Stat): boolean { + return this.species.includes(speciesId) && this.stats.includes(stat); + } +} diff --git a/src/items/held-items/survive-chance.ts b/src/items/held-items/survive-chance.ts new file mode 100644 index 00000000000..e23477f0fab --- /dev/null +++ b/src/items/held-items/survive-chance.ts @@ -0,0 +1,57 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import type { BooleanHolder } from "#app/utils/common"; +import { globalScene } from "#app/global-scene"; +import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; + +export interface SURVIVE_CHANCE_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + surviveDamage: BooleanHolder; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class SurviveChanceHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.SURVIVE_CHANCE]; + + /** + * Checks if the {@linkcode SurviveDamageModifier} should be applied + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage + * @returns `true` if the {@linkcode SurviveDamageModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, surviveDamage?: BooleanHolder): boolean { + // return super.shouldApply(pokemon, surviveDamage) && !!surviveDamage; + // } + + /** + * Applies {@linkcode SurviveDamageModifier} + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage + * @returns `true` if the survive damage has been applied + */ + apply(params: SURVIVE_CHANCE_PARAMS): boolean { + const pokemon = params.pokemon; + const surviveDamage = params.surviveDamage; + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (!surviveDamage.value && pokemon.randBattleSeedInt(10) < stackCount) { + surviveDamage.value = true; + + globalScene.phaseManager.queueMessage( + i18next.t("modifier:surviveDamageApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.name, + }), + ); + return true; + } + + return false; + } +} diff --git a/src/items/held-items/turn-end-heal.ts b/src/items/held-items/turn-end-heal.ts new file mode 100644 index 00000000000..0ba42db70ae --- /dev/null +++ b/src/items/held-items/turn-end-heal.ts @@ -0,0 +1,36 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import i18next from "i18next"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; +import { toDmgValue } from "#app/utils/common"; +import { getPokemonNameWithAffix } from "#app/messages"; + +export interface TURN_END_HEAL_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +export class TurnEndHealHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_HEAL]; + + apply(params: TURN_END_HEAL_PARAMS): boolean { + const pokemon = params.pokemon; + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (pokemon.isFullHp()) { + return false; + } + globalScene.phaseManager.unshiftPhase( + new PokemonHealPhase( + pokemon.getBattlerIndex(), + toDmgValue(pokemon.getMaxHp() / 16) * stackCount, + i18next.t("modifier:turnHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.name, + }), + true, + ), + ); + return true; + } +} diff --git a/src/items/held-items/turn-end-status.ts b/src/items/held-items/turn-end-status.ts new file mode 100644 index 00000000000..ed0a1d609fc --- /dev/null +++ b/src/items/held-items/turn-end-status.ts @@ -0,0 +1,40 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item"; +import type { StatusEffect } from "#enums/status-effect"; +import type { HeldItemId } from "#enums/held-item-id"; + +export interface TURN_END_STATUS_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class TurnEndStatusHeldItem extends HeldItem { + public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_STATUS]; + /** The status effect to be applied by the held item */ + public effect: StatusEffect; + + constructor(type: HeldItemId, maxStackCount = 1, effect: StatusEffect) { + super(type, maxStackCount); + + this.effect = effect; + } + + /** + * Tries to inflicts the holder with the associated {@linkcode StatusEffect}. + * @param pokemon {@linkcode Pokemon} that holds the held item + * @returns `true` if the status effect was applied successfully + */ + apply(params: TURN_END_STATUS_PARAMS): boolean { + return params.pokemon.trySetStatus(this.effect, true, undefined, undefined, this.name); + } + + getStatusEffect(): StatusEffect { + return this.effect; + } +} diff --git a/src/items/init-held-item-pools.ts b/src/items/init-held-item-pools.ts new file mode 100644 index 00000000000..0e6dc7b99e6 --- /dev/null +++ b/src/items/init-held-item-pools.ts @@ -0,0 +1,80 @@ +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { RewardTier } from "#enums/reward-tier"; +import { dailyStarterHeldItemPool, trainerHeldItemPool, wildHeldItemPool } from "./held-item-pool"; + +/** + * Initialize the wild held item pool + */ +function initWildHeldItemPool() { + wildHeldItemPool[RewardTier.COMMON] = [{ entry: HeldItemCategoryId.BERRY, weight: 1 }]; + wildHeldItemPool[RewardTier.GREAT] = [{ entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 1 }]; + wildHeldItemPool[RewardTier.ULTRA] = [ + { entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 5 }, + { entry: HeldItemId.WHITE_HERB, weight: 0 }, + ]; + wildHeldItemPool[RewardTier.ROGUE] = [{ entry: HeldItemId.LUCKY_EGG, weight: 4 }]; + wildHeldItemPool[RewardTier.MASTER] = [{ entry: HeldItemId.GOLDEN_EGG, weight: 1 }]; +} + +/** + * Initialize the trainer pokemon held item pool + */ +function initTrainerHeldItemPool() { + trainerHeldItemPool[RewardTier.COMMON] = [ + { entry: HeldItemCategoryId.BERRY, weight: 8 }, + { entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 3 }, + ]; + trainerHeldItemPool[RewardTier.GREAT] = [{ entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 3 }]; + trainerHeldItemPool[RewardTier.ULTRA] = [ + { entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 10 }, + { entry: HeldItemId.WHITE_HERB, weight: 0 }, + ]; + trainerHeldItemPool[RewardTier.ROGUE] = [ + { entry: HeldItemId.FOCUS_BAND, weight: 2 }, + { entry: HeldItemId.LUCKY_EGG, weight: 4 }, + { entry: HeldItemId.QUICK_CLAW, weight: 1 }, + { entry: HeldItemId.GRIP_CLAW, weight: 1 }, + { entry: HeldItemId.WIDE_LENS, weight: 1 }, + ]; + trainerHeldItemPool[RewardTier.MASTER] = [ + { entry: HeldItemId.KINGS_ROCK, weight: 1 }, + { entry: HeldItemId.LEFTOVERS, weight: 1 }, + { entry: HeldItemId.SHELL_BELL, weight: 1 }, + { entry: HeldItemId.SCOPE_LENS, weight: 1 }, + ]; +} + +/** + * Initialize the daily starter held item pool + */ +function initDailyStarterModifierPool() { + dailyStarterHeldItemPool[RewardTier.COMMON] = [ + { entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 1 }, + { entry: HeldItemCategoryId.BERRY, weight: 3 }, + ]; + dailyStarterHeldItemPool[RewardTier.GREAT] = [{ entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 5 }]; + dailyStarterHeldItemPool[RewardTier.ULTRA] = [ + { entry: HeldItemId.REVIVER_SEED, weight: 4 }, + { entry: HeldItemId.SOOTHE_BELL, weight: 1 }, + { entry: HeldItemId.SOUL_DEW, weight: 1 }, + { entry: HeldItemId.GOLDEN_PUNCH, weight: 1 }, + ]; + dailyStarterHeldItemPool[RewardTier.ROGUE] = [ + { entry: HeldItemId.GRIP_CLAW, weight: 5 }, + { entry: HeldItemId.BATON, weight: 2 }, + { entry: HeldItemId.FOCUS_BAND, weight: 5 }, + { entry: HeldItemId.QUICK_CLAW, weight: 3 }, + { entry: HeldItemId.KINGS_ROCK, weight: 3 }, + ]; + dailyStarterHeldItemPool[RewardTier.MASTER] = [ + { entry: HeldItemId.LEFTOVERS, weight: 1 }, + { entry: HeldItemId.SHELL_BELL, weight: 1 }, + ]; +} + +export function initHeldItemPools() { + // Default held item pools for specific scenarios + initWildHeldItemPool(); + initTrainerHeldItemPool(); + initDailyStarterModifierPool(); +} diff --git a/src/items/init-trainer-item-pools.ts b/src/items/init-trainer-item-pools.ts new file mode 100644 index 00000000000..40439764b0c --- /dev/null +++ b/src/items/init-trainer-item-pools.ts @@ -0,0 +1,41 @@ +import { RewardTier } from "#enums/reward-tier"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { enemyBuffTokenPool } from "./trainer-item-pool"; + +/** + * Initialize the enemy buff modifier pool + */ +function initEnemyBuffTokenPool() { + enemyBuffTokenPool[RewardTier.COMMON] = [ + { entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 9 }, + { entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 9 }, + { entry: TrainerItemId.ENEMY_ATTACK_POISON_CHANCE, weight: 3 }, + { entry: TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE, weight: 3 }, + { entry: TrainerItemId.ENEMY_ATTACK_BURN_CHANCE, weight: 3 }, + { entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 9 }, + { entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 4 }, + { entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 1 }, + ]; + enemyBuffTokenPool[RewardTier.GREAT] = [ + { entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 5 }, + { entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 5 }, + { entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 5 }, + { entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 5 }, + { entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 1 }, + ]; + enemyBuffTokenPool[RewardTier.ULTRA] = [ + { entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 10 }, + { entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 10 }, + { entry: TrainerItemId.ENEMY_HEAL, weight: 10 }, + { entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 10 }, + { entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 10 }, + { entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 5 }, + ]; + enemyBuffTokenPool[RewardTier.ROGUE] = []; + enemyBuffTokenPool[RewardTier.MASTER] = []; +} + +export function initTrainerItemPools() { + // Default held item pools for specific scenarios + initEnemyBuffTokenPool(); +} diff --git a/src/items/item-utility.ts b/src/items/item-utility.ts new file mode 100644 index 00000000000..540406acdf2 --- /dev/null +++ b/src/items/item-utility.ts @@ -0,0 +1,41 @@ +import { allHeldItems, allTrainerItems } from "#app/data/data-lists"; +import { formChangeItemName } from "#app/data/pokemon-forms"; +import type { FormChangeItem } from "#enums/form-change-item"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { TrainerItemId } from "#enums/trainer-item-id"; + +export const trainerItemSortFunc = (a: TrainerItemId, b: TrainerItemId): number => { + const itemNameMatch = allTrainerItems[a].name.localeCompare(allTrainerItems[b].name); + const itemIdMatch = a - b; + + if (itemIdMatch === 0) { + return itemNameMatch; + //Finally sort by item name + } + return itemIdMatch; +}; + +//TODO: revisit this function +export const heldItemSortFunc = (a: HeldItemId, b: HeldItemId): number => { + const itemNameMatch = allHeldItems[a].name.localeCompare(allHeldItems[b].name); + const itemIdMatch = a - b; + + if (itemIdMatch === 0) { + return itemNameMatch; + //Finally sort by item name + } + return itemIdMatch; +}; + +export const formChangeItemSortFunc = (a: FormChangeItem, b: FormChangeItem): number => { + const nameA = formChangeItemName(a); + const nameB = formChangeItemName(b); + const itemNameMatch = nameA.localeCompare(nameB); + const itemIdMatch = a - b; + + if (itemIdMatch === 0) { + return itemNameMatch; + //Finally sort by item name + } + return itemIdMatch; +}; diff --git a/src/items/modifier-to-item-migrator-utils.ts b/src/items/modifier-to-item-migrator-utils.ts new file mode 100644 index 00000000000..15682064597 --- /dev/null +++ b/src/items/modifier-to-item-migrator-utils.ts @@ -0,0 +1,152 @@ +import type ModifierData from "#app/system/modifier-data"; +import type { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; +import type { PokemonType } from "#enums/pokemon-type"; +import { Stat, type PermanentStat } from "#enums/stat"; +import type { PokemonItemMap } from "./held-item-data-types"; +import { attackTypeToHeldItem } from "./held-items/attack-type-booster"; +import { permanentStatToHeldItem } from "./held-items/base-stat-booster"; +import { berryTypeToHeldItem } from "./held-items/berry"; +import { SpeciesId } from "#enums/species-id"; + +const uniqueModifierToItem = { + EvoTrackerModifier: HeldItemId.GIMMIGHOUL_EVO_TRACKER, + PokemonBaseStatFlatModifier: HeldItemId.OLD_GATEAU, + PokemonIncrementingStatModifier: HeldItemId.MACHO_BRACE, + SurviveDamageModifier: HeldItemId.FOCUS_BAND, + BypassSpeedChanceModifier: HeldItemId.QUICK_CLAW, + FlinchChanceModifier: HeldItemId.KINGS_ROCK, + TurnHealModifier: HeldItemId.LEFTOVERS, + HitHealModifier: HeldItemId.SHELL_BELL, + PokemonInstantReviveModifier: HeldItemId.REVIVER_SEED, + ResetNegativeStatStageModifier: HeldItemId.WHITE_HERB, + FieldEffectModifier: HeldItemId.MYSTICAL_ROCK, + PokemonFriendshipBoosterModifier: HeldItemId.SOOTHE_BELL, + PokemonNatureWeightModifier: HeldItemId.SOUL_DEW, + PokemonMoveAccuracyBoosterModifier: HeldItemId.WIDE_LENS, + PokemonMultiHitModifier: HeldItemId.MULTI_LENS, + DamageMoneyRewardModifier: HeldItemId.GOLDEN_PUNCH, + SwitchEffectTransferModifier: HeldItemId.BATON, + TurnHeldItemTransferModifier: HeldItemId.MINI_BLACK_HOLE, + ContactHeldItemTransferChanceModifier: HeldItemId.GRIP_CLAW, +} as const; + +type UniqueModifierString = keyof typeof uniqueModifierToItem; + +function isUniqueModifierString(value: string): value is UniqueModifierString { + return value in uniqueModifierToItem; +} + +const modifierCategoryList = [ + "BaseStatModifier", + "EvolutionStatBoosterModifier", + "SpeciesStatBoosterModifier", + "CritBoosterModifier", + "SpeciesCritBoosterModifier", + "AttackTypeBoosterModifier", + "TurnStatusEffectModifier", + "BerryModifier", + "PokemonExpBoosterModifier", + "PokemonFormChangeItemModifier", + "PokemonBaseStatTotalModifier", +] as const; + +type ModifierCategoryString = (typeof modifierCategoryList)[number]; + +function isModifierCategoryString(value: string): value is ModifierCategoryString { + return modifierCategoryList.includes(value as ModifierCategoryString); +} + +function mapModifierCategoryToItems(modifier: ModifierCategoryString, typeId: string, args: any): HeldItemId { + if (modifier === "BaseStatModifier") { + const stat = args[1] as PermanentStat; + return permanentStatToHeldItem[stat]; + } + if (modifier === "EvolutionStatBoosterModifier") { + return HeldItemId.EVIOLITE; + } + if (modifier === "SpeciesStatBoosterModifier") { + const stats = args[1]; + const species = args[3]; + if (SpeciesId.PIKACHU in species) { + return HeldItemId.LIGHT_BALL; + } + if (SpeciesId.CUBONE in species) { + return HeldItemId.THICK_CLUB; + } + if (SpeciesId.DITTO in species && Stat.DEF in stats) { + return HeldItemId.METAL_POWDER; + } + if (SpeciesId.DITTO in species && Stat.SPDEF in stats) { + return HeldItemId.QUICK_POWDER; + } + if (SpeciesId.CLAMPERL in species && Stat.SPDEF in stats) { + return HeldItemId.DEEP_SEA_SCALE; + } + if (SpeciesId.CLAMPERL in species && Stat.SPATK in stats) { + return HeldItemId.DEEP_SEA_TOOTH; + } + } + if (modifier === "CritBoosterModifier") { + return HeldItemId.SCOPE_LENS; + } + if (modifier === "SpeciesCritBoosterModifier") { + return HeldItemId.LEEK; + } + if (modifier === "AttackTypeBoosterModifier") { + const moveType = args[1] as PokemonType; + return attackTypeToHeldItem[moveType]; + } + if (modifier === "TurnStatusEffectModifier") { + switch (typeId) { + case "TOXIC_ORB": + return HeldItemId.TOXIC_ORB; + case "FLAME_ORB": + return HeldItemId.FLAME_ORB; + } + } + if (modifier === "BerryModifier") { + const berryType = args[1] as BerryType; + return berryTypeToHeldItem[berryType]; + } + if (modifier === "PokemonExpBoosterModifier") { + const boost = args[1] as number; + return boost === 100 ? HeldItemId.GOLDEN_EGG : HeldItemId.LUCKY_EGG; + } + if (modifier === "PokemonBaseStatTotalModifier") { + const statModifier = args[1] as number; + return statModifier > 0 ? HeldItemId.SHUCKLE_JUICE_GOOD : HeldItemId.SHUCKLE_JUICE_BAD; + } + return 0; +} + +export function convertModifierSaveData(data: ModifierData[]) { + const pokemonItems: PokemonItemMap[] = []; + for (const entry of data) { + const typeId = entry.typeId; + const args = entry.args; + const pokemonId = args[0]; + const stack = entry.stackCount; + const className = entry.className; + + if (className === "PokemonFormChangeItemModifier") { + } + //TODO: Code to filter out modifiers which are not held items + + let itemId: HeldItemId = 0; + + if (isModifierCategoryString(className)) { + itemId = mapModifierCategoryToItems(className, typeId, args); + } + + if (isUniqueModifierString(className)) { + itemId = uniqueModifierToItem[className]; + } + + if (itemId) { + const specs = { id: itemId, stack: stack }; + const pokemonItem = { item: specs, pokemonId: pokemonId }; + pokemonItems.push(pokemonItem); + } + } +} diff --git a/src/items/trainer-item-data-types.ts b/src/items/trainer-item-data-types.ts new file mode 100644 index 00000000000..a024c4c362f --- /dev/null +++ b/src/items/trainer-item-data-types.ts @@ -0,0 +1,44 @@ +import type { RewardTier } from "#enums/reward-tier"; +import type { TrainerItemId } from "#enums/trainer-item-id"; + +export type TrainerItemData = { + stack: number; + disabled?: boolean; + cooldown?: number; +}; + +export type TrainerItemDataMap = { + [key in TrainerItemId]?: TrainerItemData; +}; + +export type TrainerItemSpecs = TrainerItemData & { + id: TrainerItemId; +}; + +export function isTrainerItemSpecs(entry: any): entry is TrainerItemSpecs { + return typeof entry.id === "number" && "stack" in entry; +} + +type TrainerItemPoolEntry = { + entry: TrainerItemId; + weight: number; +}; + +export type TrainerItemPool = TrainerItemPoolEntry[]; + +export type TrainerItemTieredPool = { + [key in RewardTier]?: TrainerItemPool; +}; + +export function isTrainerItemPool(value: any): value is TrainerItemPool { + return Array.isArray(value) && value.every(entry => "entry" in entry && "weight" in entry); +} + +type TrainerItemConfigurationEntry = { + entry: TrainerItemId | TrainerItemSpecs; + count?: number | (() => number); +}; + +export type TrainerItemConfiguration = TrainerItemConfigurationEntry[]; + +export type TrainerItemSaveData = TrainerItemSpecs[]; diff --git a/src/items/trainer-item-manager.ts b/src/items/trainer-item-manager.ts new file mode 100644 index 00000000000..04da440cde6 --- /dev/null +++ b/src/items/trainer-item-manager.ts @@ -0,0 +1,158 @@ +import { allTrainerItems } from "#app/data/data-lists"; +import type { TrainerItemId } from "#app/enums/trainer-item-id"; +import { + isTrainerItemSpecs, + type TrainerItemSaveData, + type TrainerItemConfiguration, + type TrainerItemDataMap, + type TrainerItemSpecs, +} from "#app/items/trainer-item-data-types"; + +export class TrainerItemManager { + public trainerItems: TrainerItemDataMap; + + constructor() { + this.trainerItems = {}; + } + + getItemSpecs(id: TrainerItemId): TrainerItemSpecs | undefined { + const item = this.trainerItems[id]; + if (item) { + const itemSpecs: TrainerItemSpecs = { + ...item, + id, + }; + return itemSpecs; + } + return undefined; + } + + generateTrainerItemConfiguration(restrictedIds?: TrainerItemId[]): TrainerItemConfiguration { + const config: TrainerItemConfiguration = []; + for (const [k, item] of Object.entries(this.trainerItems)) { + const id = Number(k); + if (item && (!restrictedIds || id in restrictedIds)) { + const specs: TrainerItemSpecs = { ...item, id }; + config.push({ entry: specs, count: 1 }); + } + } + return config; + } + + generateSaveData(): TrainerItemSaveData { + const saveData: TrainerItemSaveData = []; + for (const [k, item] of Object.entries(this.trainerItems)) { + const id = Number(k); + if (item) { + const specs: TrainerItemSpecs = { ...item, id }; + saveData.push(specs); + } + } + return saveData; + } + + getTrainerItems(): number[] { + return Object.keys(this.trainerItems).map(k => Number(k)); + } + + hasItem(itemType: TrainerItemId): boolean { + return itemType in this.trainerItems; + } + + getStack(itemType: TrainerItemId): number { + const item = this.trainerItems[itemType]; + return item ? item.stack : 0; + } + + isMaxStack(itemType: TrainerItemId): boolean { + const item = this.trainerItems[itemType]; + return item ? item.stack >= allTrainerItems[itemType].getMaxStackCount() : false; + } + + overrideItems(newItems: TrainerItemDataMap) { + this.trainerItems = newItems; + // The following is to allow randomly generated item configs to have stack 0 + for (const [item, properties] of Object.entries(this.trainerItems)) { + if (!properties || properties.stack <= 0) { + delete this.trainerItems[item]; + } + } + } + + add(itemType: TrainerItemId | TrainerItemSpecs, addStack?: number): boolean { + if (isTrainerItemSpecs(itemType)) { + return this.addItemWithSpecs(itemType); + } + + if (!addStack) { + addStack = allTrainerItems[itemType].isLapsing ? allTrainerItems[itemType].getMaxStackCount() : 1; + } + const maxStack = allTrainerItems[itemType].getMaxStackCount(); + const item = this.trainerItems[itemType]; + + if (item) { + // TODO: We may want an error message of some kind instead + if (item.stack < maxStack) { + item.stack = Math.min(item.stack + addStack, maxStack); + return true; + } + } else { + this.trainerItems[itemType] = { stack: Math.min(addStack, maxStack) }; + return true; + } + return false; + } + + addItemWithSpecs(itemSpecs: TrainerItemSpecs): boolean { + const id = itemSpecs.id; + const maxStack = allTrainerItems[id].getMaxStackCount(); + const item = this.trainerItems[id]; + + const tempStack = item?.stack ?? 0; + + this.trainerItems[id] = itemSpecs; + this.trainerItems[id].stack = Math.min(itemSpecs.stack + tempStack, maxStack); + + return true; + } + + remove(itemType: TrainerItemId, removeStack = 1, all = false) { + const item = this.trainerItems[itemType]; + + if (item) { + item.stack -= removeStack; + + if (all || item.stack <= 0) { + delete this.trainerItems[itemType]; + } + } + } + + filterRequestedItems(requestedItems: TrainerItemId[], exclude = false) { + const currentItems = this.getTrainerItems(); + return currentItems.filter(it => !exclude && requestedItems.some(entry => it === entry)); + } + + getTrainerItemCount(): number { + let total = 0; + for (const properties of Object.values(this.trainerItems)) { + total += properties?.stack ?? 0; + } + return total; + } + + lapseItems(): void { + for (const [item, properties] of Object.entries(this.trainerItems)) { + if (allTrainerItems[item].isLapsing && properties) { + properties.stack -= 1; + } + if (!properties || properties.stack <= 0) { + delete this.trainerItems[item]; + } + } + } + + clearItems() { + this.trainerItems = {}; + } +} diff --git a/src/items/trainer-item-pool.ts b/src/items/trainer-item-pool.ts new file mode 100644 index 00000000000..86a87925325 --- /dev/null +++ b/src/items/trainer-item-pool.ts @@ -0,0 +1,62 @@ +import { globalScene } from "#app/global-scene"; +import { isNullOrUndefined, pickWeightedIndex } from "#app/utils/common"; +import { RewardTier } from "#enums/reward-tier"; +import type { TrainerItemId } from "#enums/trainer-item-id"; +import { allTrainerItems } from "./all-trainer-items"; +import type { TrainerItemPool, TrainerItemTieredPool } from "./trainer-item-data-types"; +import type { TrainerItemManager } from "./trainer-item-manager"; + +export const enemyBuffTokenPool: TrainerItemTieredPool = {}; + +function getPoolWeights(pool: TrainerItemPool, manager: TrainerItemManager): number[] { + return pool.map(p => { + if (manager.isMaxStack(p.entry)) { + return 0; + } + return p.weight; + }); +} + +export function getNewTrainerItemFromPool(pool: TrainerItemPool, manager: TrainerItemManager): TrainerItemId { + const weights = getPoolWeights(pool, manager); + + const pickedIndex = pickWeightedIndex(weights); + if (isNullOrUndefined(pickedIndex)) { + return 0; + } + const entry = pool[pickedIndex].entry; + + return entry as TrainerItemId; +} + +export function assignEnemyBuffTokenForWave(tier: RewardTier) { + let tierStackCount: number; + switch (tier) { + case RewardTier.ULTRA: + tierStackCount = 5; + break; + case RewardTier.GREAT: + tierStackCount = 3; + break; + default: + tierStackCount = 1; + break; + } + + if (!enemyBuffTokenPool[tier]) { + return; + } + + const retryCount = 50; + let candidate = getNewTrainerItemFromPool(enemyBuffTokenPool[tier], globalScene.enemyTrainerItems); + let r = 0; + while ( + ++r < retryCount && + allTrainerItems[candidate].getMaxStackCount() < + globalScene.enemyTrainerItems.getStack(candidate) + (r < 10 ? tierStackCount : 1) + ) { + candidate = getNewTrainerItemFromPool(enemyBuffTokenPool[tier], globalScene.enemyTrainerItems); + } + + globalScene.enemyTrainerItems.add(candidate, tierStackCount); +} diff --git a/src/items/trainer-item.ts b/src/items/trainer-item.ts new file mode 100644 index 00000000000..a5877a9ad97 --- /dev/null +++ b/src/items/trainer-item.ts @@ -0,0 +1,575 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { hslToHex, randSeedFloat, toDmgValue, type BooleanHolder, type NumberHolder } from "#app/utils/common"; +import { TrainerItemId, TrainerItemNames } from "#enums/trainer-item-id"; +import i18next from "i18next"; +import type { TrainerItemManager } from "./trainer-item-manager"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { getStatKey, Stat, type TempBattleStat } from "#enums/stat"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { StatusEffect } from "#enums/status-effect"; + +export const TRAINER_ITEM_EFFECT = { + LEVEL_INCREMENT_BOOSTER: 1, + PRESERVE_BERRY: 2, + HEALING_BOOSTER: 3, + EXP_BOOSTER: 4, + MONEY_MULTIPLIER: 5, + HIDDEN_ABILITY_CHANCE_BOOSTER: 6, + SHINY_RATE_BOOSTER: 7, + CRITICAL_CATCH_CHANCE_BOOSTER: 8, + EXTRA_REWARD: 9, + + HEAL_SHOP_COST: 10, + DOUBLE_BATTLE_CHANCE_BOOSTER: 11, + + TEMP_STAT_STAGE_BOOSTER: 12, + TEMP_ACCURACY_BOOSTER: 13, + TEMP_CRIT_BOOSTER: 14, + + ENEMY_DAMAGE_BOOSTER: 15, + ENEMY_DAMAGE_REDUCER: 16, + ENEMY_HEAL: 17, + ENEMY_ATTACK_STATUS_CHANCE: 18, + ENEMY_STATUS_HEAL_CHANCE: 19, + ENEMY_ENDURE_CHANCE: 20, + ENEMY_FUSED_CHANCE: 21, +} as const; + +export type TRAINER_ITEM_EFFECT = (typeof TRAINER_ITEM_EFFECT)[keyof typeof TRAINER_ITEM_EFFECT]; + +export interface NUMBER_HOLDER_PARAMS { + numberHolder: NumberHolder; +} + +export interface BOOLEAN_HOLDER_PARAMS { + booleanHolder: BooleanHolder; +} + +export interface POKEMON_PARAMS { + pokemon: Pokemon; +} + +export class TrainerItem { + // public pokemonId: number; + public type: TrainerItemId; + public maxStackCount: number; + public isLapsing = false; + public effects: TRAINER_ITEM_EFFECT[] = []; + + //TODO: If this is actually never changed by any subclass, perhaps it should not be here + public soundName = "se/restore"; + + constructor(type: TrainerItemId, maxStackCount = 1) { + this.type = type; + this.maxStackCount = maxStackCount; + } + + get name(): string { + return i18next.t(`modifierType:ModifierType.${TrainerItemNames[this.type]}.name`); + } + + get description(): string { + return i18next.t(`modifierType:ModifierType.${TrainerItemNames[this.type]}.description`); + } + + get iconName(): string { + return `${TrainerItemNames[this.type]?.toLowerCase()}`; + } + + getMaxStackCount(): number { + return this.maxStackCount; + } + + createIcon(stackCount: number): Phaser.GameObjects.Container { + const container = globalScene.add.container(0, 0); + + const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5); + container.add(item); + + const stackText = this.getIconStackText(stackCount); + if (stackText) { + container.add(stackText); + } + + return container; + } + + getIconStackText(stackCount: number): Phaser.GameObjects.BitmapText | null { + if (this.getMaxStackCount() === 1 || stackCount < 1) { + return null; + } + + const text = globalScene.add.bitmapText(10, 15, "item-count", stackCount.toString(), 11); + text.letterSpacing = -0.5; + if (stackCount >= this.getMaxStackCount()) { + text.setTint(0xf89890); + } + text.setOrigin(0); + + return text; + } + + getScoreMultiplier(): number { + return 1; + } +} + +// Candy Jar +export class LevelIncrementBoosterTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.LEVEL_INCREMENT_BOOSTER]; + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const count = params.numberHolder; + const stack = manager.getStack(this.type); + count.value += stack; + } +} + +// Berry Pouch +export interface PRESERVE_BERRY_PARAMS { + pokemon: Pokemon; + doPreserve: BooleanHolder; +} + +export class PreserveBerryTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.PRESERVE_BERRY]; + + apply(manager: TrainerItemManager, params: PRESERVE_BERRY_PARAMS) { + const stack = manager.getStack(this.type); + params.doPreserve.value ||= params.pokemon.randBattleSeedInt(10) < stack * 3; + } +} + +// Healing Charm +export class HealingBoosterTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.HEALING_BOOSTER]; + private multiplier: number; + + constructor(type: TrainerItemId, multiplier: number, stackCount?: number) { + super(type, stackCount); + + this.multiplier = multiplier; + } + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const healingMultiplier = params.numberHolder; + const stack = manager.getStack(this.type); + healingMultiplier.value *= 1 + (this.multiplier - 1) * stack; + } +} + +// Exp Booster +export class ExpBoosterTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.EXP_BOOSTER]; + private boostPercent: number; + + constructor(type: TrainerItemId, boostPercent: number, stackCount?: number) { + super(type, stackCount); + + this.boostPercent = boostPercent; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.ExpBoosterModifierType.description", { + boostPercent: this.boostPercent, + }); + } + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const boost = params.numberHolder; + const stack = manager.getStack(this.type); + boost.value = Math.floor(boost.value * (1 + stack * this.boostPercent * 0.01)); + } +} + +export class MoneyMultiplierTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER]; + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const moneyMultiplier = params.numberHolder; + const stack = manager.getStack(this.type); + moneyMultiplier.value += Math.floor(moneyMultiplier.value * 0.2 * stack); + } +} + +export class HiddenAbilityChanceBoosterTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER]; + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const boost = params.numberHolder; + const stack = manager.getStack(this.type); + boost.value *= Math.pow(2, -1 - stack); + } +} + +export class ShinyRateBoosterTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER]; + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const boost = params.numberHolder; + const stack = manager.getStack(this.type); + boost.value *= Math.pow(2, 1 + stack); + } +} + +export class CriticalCatchChanceBoosterTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.CRITICAL_CATCH_CHANCE_BOOSTER]; + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const boost = params.numberHolder; + const stack = manager.getStack(this.type); + boost.value *= 1.5 + stack / 2; + } +} + +export class ExtraRewardTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.EXTRA_REWARD]; + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const count = params.numberHolder; + const stack = manager.getStack(this.type); + count.value += stack; + } +} + +export class HealShopCostTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.HEAL_SHOP_COST]; + public readonly shopMultiplier: number; + + constructor(type: TrainerItemId, shopMultiplier: number, stackCount?: number) { + super(type, stackCount); + + this.shopMultiplier = shopMultiplier; + } + + apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const moneyCost = params.numberHolder; + moneyCost.value = Math.floor(moneyCost.value * this.shopMultiplier); + } +} + +export class LapsingTrainerItem extends TrainerItem { + isLapsing = true; + + createIcon(battleCount: number): Phaser.GameObjects.Container { + const container = globalScene.add.container(0, 0); + + const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5); + container.add(item); + + // Linear interpolation on hue + const hue = Math.floor(120 * (battleCount / this.getMaxStackCount()) + 5); + + // Generates the color hex code with a constant saturation and lightness but varying hue + const typeHex = hslToHex(hue, 0.5, 0.9); + const strokeHex = hslToHex(hue, 0.7, 0.3); + + const battleCountText = addTextObject(27, 0, battleCount.toString(), TextStyle.PARTY, { + fontSize: "66px", + color: typeHex, + }); + battleCountText.setShadow(0, 0); + battleCountText.setStroke(strokeHex, 16); + battleCountText.setOrigin(1, 0); + container.add(battleCountText); + + return container; + } +} + +export class DoubleBattleChanceBoosterTrainerItem extends LapsingTrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.DOUBLE_BATTLE_CHANCE_BOOSTER]; + + get description(): string { + return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", { + battleCount: this.getMaxStackCount(), + }); + } + + apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const doubleBattleChance = params.numberHolder; + // This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using randSeedInt + // A double battle will initiate if the generated number is 0 + doubleBattleChance.value /= 4; + } +} + +interface TempStatToTrainerItemMap { + [key: number]: TrainerItemId; +} + +export const tempStatToTrainerItem: TempStatToTrainerItemMap = { + [Stat.ATK]: TrainerItemId.X_ATTACK, + [Stat.DEF]: TrainerItemId.X_DEFENSE, + [Stat.SPATK]: TrainerItemId.X_SP_ATK, + [Stat.SPDEF]: TrainerItemId.X_SP_DEF, + [Stat.SPD]: TrainerItemId.X_SPEED, + [Stat.ACC]: TrainerItemId.X_ACCURACY, +}; + +export class TempStatStageBoosterTrainerItem extends LapsingTrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.TEMP_STAT_STAGE_BOOSTER]; + private stat: TempBattleStat; + + constructor(type: TrainerItemId, stat: TempBattleStat, stackCount?: number) { + super(type, stackCount); + + this.stat = stat; + } + + get name(): string { + return i18next.t(`modifierType:TempStatStageBoosterItem.${TrainerItemNames[this.type]?.toLowerCase()}`); + } + + get description(): string { + console.log(); + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { + stat: i18next.t(getStatKey(this.stat)), + amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.percentage"), + }); + } + + apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const statLevel = params.numberHolder; + const boost = 0.3; + statLevel.value += boost; + } +} + +export class TempAccuracyBoosterTrainerItem extends LapsingTrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.TEMP_ACCURACY_BOOSTER]; + + get name(): string { + return i18next.t(`modifierType:TempStatStageBoosterItem.${TrainerItemNames[this.type]?.toLowerCase()}`); + } + + get description(): string { + console.log(); + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { + stat: i18next.t(getStatKey(Stat.ACC)), + amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.percentage"), + }); + } + + apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const statLevel = params.numberHolder; + const boost = 1; + statLevel.value += boost; + } +} + +export class TempCritBoosterTrainerItem extends LapsingTrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.TEMP_CRIT_BOOSTER]; + + get description(): string { + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { + stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises"), + amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.stage"), + }); + } + + apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) { + const critLevel = params.numberHolder; + critLevel.value++; + } +} + +export class EnemyDamageBoosterTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_BOOSTER]; + public damageBoost = 1.05; + + get iconName(): string { + return "wl_item_drop"; + } + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS): boolean { + const stack = manager.getStack(this.type); + const multiplier = params.numberHolder; + + multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageBoost, stack)); + + return true; + } + + getMaxStackCount(): number { + return 999; + } +} + +export class EnemyDamageReducerTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_REDUCER]; + public damageReduction = 0.975; + + get iconName(): string { + return "wl_guard_spec"; + } + + apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS): boolean { + const stack = manager.getStack(this.type); + const multiplier = params.numberHolder; + + multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageReduction, stack)); + + return true; + } + + getMaxStackCount(): number { + return globalScene.currentBattle.waveIndex < 2000 ? 99 : 999; + } +} + +export class EnemyTurnHealTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_HEAL]; + public healPercent = 2; + + get iconName(): string { + return "wl_potion"; + } + + apply(manager: TrainerItemManager, params: POKEMON_PARAMS): boolean { + const stack = manager.getStack(this.type); + const enemyPokemon = params.pokemon; + + if (!enemyPokemon.isFullHp()) { + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + enemyPokemon.getBattlerIndex(), + Math.max(Math.floor(enemyPokemon.getMaxHp() / (100 / this.healPercent)) * stack, 1), + i18next.t("modifier:enemyTurnHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), + }), + true, + false, + false, + false, + true, + ); + return true; + } + + return false; + } +} + +export class EnemyAttackStatusEffectChanceTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_ATTACK_STATUS_CHANCE]; + public effect: StatusEffect; + + constructor(type: TrainerItemId, effect: StatusEffect, stackCount?: number) { + super(type, stackCount); + + this.effect = effect; + } + + get iconName(): string { + if (this.effect === StatusEffect.POISON) { + return "wl_antidote"; + } + if (this.effect === StatusEffect.PARALYSIS) { + return "wl_paralyze_heal"; + } + if (this.effect === StatusEffect.BURN) { + return "wl_burn_heal"; + } + return ""; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.EnemyAttackStatusEffectChanceModifierType.description", { + chancePercent: this.getChance() * 100, + statusEffect: getStatusEffectDescriptor(this.effect), + }); + } + + apply(manager: TrainerItemManager, params: POKEMON_PARAMS): boolean { + const stack = manager.getStack(this.type); + const enemyPokemon = params.pokemon; + const chance = this.getChance(); + + if (randSeedFloat() <= chance * stack) { + return enemyPokemon.trySetStatus(this.effect, true); + } + + return false; + } + + getChance(): number { + return 0.025 * (this.effect === StatusEffect.BURN || this.effect === StatusEffect.POISON ? 2 : 1); + } +} + +export class EnemyStatusEffectHealChanceTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_STATUS_HEAL_CHANCE]; + public chance = 0.025; + + get iconName(): string { + return "wl_full_heal"; + } + + apply(manager: TrainerItemManager, params: POKEMON_PARAMS): boolean { + const stack = manager.getStack(this.type); + const enemyPokemon = params.pokemon; + + if (!enemyPokemon.status || randSeedFloat() > this.chance * stack) { + return false; + } + + globalScene.phaseManager.queueMessage( + getStatusEffectHealText(enemyPokemon.status.effect, getPokemonNameWithAffix(enemyPokemon)), + ); + enemyPokemon.resetStatus(); + enemyPokemon.updateInfo(); + return true; + } +} + +export class EnemyEndureChanceTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_ENDURE_CHANCE]; + public chance = 2; + + get iconName(): string { + return "wl_reset_urge"; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.EnemyEndureChanceModifierType.description", { + chancePercent: this.chance, + }); + } + + apply(manager: TrainerItemManager, params: POKEMON_PARAMS): boolean { + const stack = manager.getStack(this.type); + const target = params.pokemon; + + if (target.waveData.endured || target.randBattleSeedInt(100) >= this.chance * stack) { + return false; + } + + target.addTag(BattlerTagType.ENDURE_TOKEN, 1); + + target.waveData.endured = true; + + return true; + } +} + +export class EnemyFusionChanceTrainerItem extends TrainerItem { + public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_FUSED_CHANCE]; + public chance = 0.01; + + get iconName(): string { + return "wl_custom_spliced"; + } + + apply(manager: TrainerItemManager, params: BOOLEAN_HOLDER_PARAMS) { + const stack = manager.getStack(this.type); + const isFusion = params.booleanHolder; + if (randSeedFloat() > this.chance * stack) { + return false; + } + isFusion.value = true; + } +} diff --git a/src/loading-scene.ts b/src/loading-scene.ts index f67d19e1027..bcc0cdbc1fc 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -21,8 +21,12 @@ import { initVouchers } from "#app/system/voucher"; import { BiomeId } from "#enums/biome-id"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { timedEventManager } from "./global-event-manager"; +import { initHeldItems } from "./items/all-held-items"; import { initModifierPools } from "./modifier/init-modifier-pools"; import { initModifierTypes } from "./modifier/modifier-type"; +import { initHeldItemPools } from "./items/init-held-item-pools"; +import { initTrainerItemPools } from "./items/init-trainer-item-pools"; +import { initTrainerItems } from "./items/all-trainer-items"; export class LoadingScene extends SceneBase { public static readonly KEY = "loading"; @@ -367,6 +371,8 @@ export class LoadingScene extends SceneBase { initModifierTypes(); initModifierPools(); + initHeldItemPools(); + initTrainerItemPools(); initAchievements(); initVouchers(); @@ -380,6 +386,8 @@ export class LoadingScene extends SceneBase { initSpecies(); initMoves(); initAbilities(); + initHeldItems(); + initTrainerItems(); initChallenges(); initMysteryEncounters(); } diff --git a/src/modifier/init-modifier-pools.ts b/src/modifier/init-modifier-pools.ts index 60697333600..afaf34e7af9 100644 --- a/src/modifier/init-modifier-pools.ts +++ b/src/modifier/init-modifier-pools.ts @@ -1,20 +1,11 @@ import type Pokemon from "#app/field/pokemon"; -import { - dailyStarterModifierPool, - enemyBuffModifierPool, - modifierPool, - trainerModifierPool, - wildModifierPool, -} from "#app/modifier/modifier-pools"; +import { modifierPool } from "#app/modifier/modifier-pools"; import { globalScene } from "#app/global-scene"; -import { DoubleBattleChanceBoosterModifier, SpeciesCritBoosterModifier, TurnStatusEffectModifier } from "./modifier"; import { WeightedModifierType } from "./modifier-type"; -import { ModifierTier } from "../enums/modifier-tier"; +import { RewardTier } from "#app/enums/reward-tier"; import type { WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types"; import { modifierTypes } from "#app/data/data-lists"; import { PokeballType } from "#enums/pokeball"; -import { BerryModifier } from "./modifier"; -import { BerryType } from "#enums/berry-type"; import { SpeciesId } from "#enums/species-id"; import { timedEventManager } from "#app/global-event-manager"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; @@ -26,41 +17,17 @@ import { AbilityId } from "#enums/ability-id"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; // biome-ignore lint/correctness/noUnusedImports: This is used in a tsdoc comment import type { initModifierTypes } from "./modifier-type"; - -/** - * Initialize the wild modifier pool - */ -function initWildModifierPool() { - wildModifierPool[ModifierTier.COMMON] = [new WeightedModifierType(modifierTypes.BERRY, 1)].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }); - wildModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }); - wildModifierPool[ModifierTier.ULTRA] = [ - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.WHITE_HERB, 0), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }); - wildModifierPool[ModifierTier.ROGUE] = [new WeightedModifierType(modifierTypes.LUCKY_EGG, 4)].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }); - wildModifierPool[ModifierTier.MASTER] = [new WeightedModifierType(modifierTypes.GOLDEN_EGG, 1)].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }); -} +import { HeldItemId } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { allTrainerItems } from "#app/data/data-lists"; +import type { TurnEndStatusHeldItem } from "#app/items/held-items/turn-end-status"; /** * Initialize the common modifier pool */ function initCommonModifierPool() { - modifierPool[ModifierTier.COMMON] = [ + modifierPool[RewardTier.COMMON] = [ new WeightedModifierType(modifierTypes.POKEBALL, () => (hasMaximumBalls(PokeballType.POKEBALL) ? 0 : 6), 6), new WeightedModifierType(modifierTypes.RARE_CANDY, 2), new WeightedModifierType( @@ -92,7 +59,7 @@ function initCommonModifierPool() { party.filter( p => p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && + !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) && p .getMoveset() .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) @@ -111,7 +78,7 @@ function initCommonModifierPool() { party.filter( p => p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && + !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) && p .getMoveset() .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) @@ -123,12 +90,12 @@ function initCommonModifierPool() { }, 3, ), - new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), + new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(TrainerItemId.LURE, 2)), new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4), new WeightedModifierType(modifierTypes.BERRY, 2), new WeightedModifierType(modifierTypes.TM_COMMON, 2), ].map(m => { - m.setTier(ModifierTier.COMMON); + m.setTier(RewardTier.COMMON); return m; }); } @@ -137,7 +104,7 @@ function initCommonModifierPool() { * Initialize the Great modifier pool */ function initGreatModifierPool() { - modifierPool[ModifierTier.GREAT] = [ + modifierPool[RewardTier.GREAT] = [ new WeightedModifierType(modifierTypes.GREAT_BALL, () => (hasMaximumBalls(PokeballType.GREAT_BALL) ? 0 : 6), 6), new WeightedModifierType(modifierTypes.PP_UP, 2), new WeightedModifierType( @@ -148,12 +115,10 @@ function initGreatModifierPool() { p => p.hp && !!p.status && - !p.getHeldItems().some(i => { - if (i instanceof TurnStatusEffectModifier) { - return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; - } - return false; - }), + !p + .getHeldItems() + .filter(i => i in [HeldItemId.TOXIC_ORB, HeldItemId.FLAME_ORB]) + .some(i => (allHeldItems[i] as TurnEndStatusHeldItem).effect === p.status?.effect), ).length, 3, ); @@ -214,12 +179,10 @@ function initGreatModifierPool() { p => p.hp && !!p.status && - !p.getHeldItems().some(i => { - if (i instanceof TurnStatusEffectModifier) { - return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; - } - return false; - }), + !p + .getHeldItems() + .filter(i => i in [HeldItemId.TOXIC_ORB, HeldItemId.FLAME_ORB]) + .some(i => (allHeldItems[i] as TurnEndStatusHeldItem).effect === p.status?.effect), ).length, 3, ); @@ -239,7 +202,7 @@ function initGreatModifierPool() { party.filter( p => p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && + !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) && p .getMoveset() .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) @@ -258,7 +221,7 @@ function initGreatModifierPool() { party.filter( p => p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && + !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) && p .getMoveset() .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) @@ -271,7 +234,7 @@ function initGreatModifierPool() { 3, ), new WeightedModifierType(modifierTypes.DIRE_HIT, 4), - new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(15, 4)), + new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(TrainerItemId.SUPER_LURE, 4)), new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)), new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 4), new WeightedModifierType( @@ -331,7 +294,7 @@ function initGreatModifierPool() { 1, ), ].map(m => { - m.setTier(ModifierTier.GREAT); + m.setTier(RewardTier.GREAT); return m; }); } @@ -340,9 +303,9 @@ function initGreatModifierPool() { * Initialize the Ultra modifier pool */ function initUltraModifierPool() { - modifierPool[ModifierTier.ULTRA] = [ + modifierPool[RewardTier.ULTRA] = [ new WeightedModifierType(modifierTypes.ULTRA_BALL, () => (hasMaximumBalls(PokeballType.ULTRA_BALL) ? 0 : 15), 15), - new WeightedModifierType(modifierTypes.MAX_LURE, lureWeightFunc(30, 4)), + new WeightedModifierType(modifierTypes.MAX_LURE, lureWeightFunc(TrainerItemId.MAX_LURE, 4)), new WeightedModifierType(modifierTypes.BIG_NUGGET, skipInLastClassicWaveOrDefault(12)), new WeightedModifierType(modifierTypes.PP_MAX, 3), new WeightedModifierType(modifierTypes.MINT, 4), @@ -368,7 +331,7 @@ function initUltraModifierPool() { (p.isFusion() && p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)) ) { // Check if Pokemon is already holding an Eviolite - return !p.getHeldItems().some(i => i.type.id === "EVIOLITE"); + return !p.heldItemManager.hasItem(HeldItemId.EVIOLITE); } return false; }) @@ -385,7 +348,7 @@ function initUltraModifierPool() { // If a party member doesn't already have a Leek and is one of the relevant species, Leek can appear return party.some( p => - !p.getHeldItems().some(i => i instanceof SpeciesCritBoosterModifier) && + !p.heldItemManager.hasItem(HeldItemId.LEEK) && (checkedSpecies.includes(p.getSpeciesForm(true).speciesId) || (p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId))), ) @@ -398,7 +361,7 @@ function initUltraModifierPool() { modifierTypes.TOXIC_ORB, (party: Pokemon[]) => { return party.some(p => { - const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); + const isHoldingOrb = p.getHeldItems().some(i => i in [HeldItemId.FLAME_ORB, HeldItemId.TOXIC_ORB]); if (!isHoldingOrb) { const moveset = p @@ -444,7 +407,7 @@ function initUltraModifierPool() { modifierTypes.FLAME_ORB, (party: Pokemon[]) => { return party.some(p => { - const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); + const isHoldingOrb = p.getHeldItems().some(i => i in [HeldItemId.FLAME_ORB, HeldItemId.TOXIC_ORB]); if (!isHoldingOrb) { const moveset = p @@ -490,13 +453,8 @@ function initUltraModifierPool() { modifierTypes.MYSTICAL_ROCK, (party: Pokemon[]) => { return party.some(p => { - let isHoldingMax = false; - for (const i of p.getHeldItems()) { - if (i.type.id === "MYSTICAL_ROCK") { - isHoldingMax = i.getStackCount() === i.getMaxStackCount(); - break; - } - } + const stack = p.heldItemManager.getStack(HeldItemId.MYSTICAL_ROCK); + const isHoldingMax = stack === allHeldItems[HeldItemId.MYSTICAL_ROCK].maxStackCount; if (!isHoldingMax) { const moveset = p.getMoveset(true).map(m => m.moveId); @@ -558,13 +516,13 @@ function initUltraModifierPool() { new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), new WeightedModifierType(modifierTypes.WIDE_LENS, 7), ].map(m => { - m.setTier(ModifierTier.ULTRA); + m.setTier(RewardTier.ULTRA); return m; }); } function initRogueModifierPool() { - modifierPool[ModifierTier.ROGUE] = [ + modifierPool[RewardTier.ROGUE] = [ new WeightedModifierType(modifierTypes.ROGUE_BALL, () => (hasMaximumBalls(PokeballType.ROGUE_BALL) ? 0 : 16), 16), new WeightedModifierType(modifierTypes.RELIC_GOLD, skipInLastClassicWaveOrDefault(2)), new WeightedModifierType(modifierTypes.LEFTOVERS, 3), @@ -602,7 +560,7 @@ function initRogueModifierPool() { 3, ), ].map(m => { - m.setTier(ModifierTier.ROGUE); + m.setTier(RewardTier.ROGUE); return m; }); } @@ -611,7 +569,7 @@ function initRogueModifierPool() { * Initialize the Master modifier pool */ function initMasterModifierPool() { - modifierPool[ModifierTier.MASTER] = [ + modifierPool[RewardTier.MASTER] = [ new WeightedModifierType(modifierTypes.MASTER_BALL, () => (hasMaximumBalls(PokeballType.MASTER_BALL) ? 0 : 24), 24), new WeightedModifierType(modifierTypes.SHINY_CHARM, 14), new WeightedModifierType(modifierTypes.HEALING_CHARM, 18), @@ -644,140 +602,7 @@ function initMasterModifierPool() { 1, ), ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }); -} - -function initTrainerModifierPool() { - trainerModifierPool[ModifierTier.COMMON] = [ - new WeightedModifierType(modifierTypes.BERRY, 8), - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }); - trainerModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }); - trainerModifierPool[ModifierTier.ULTRA] = [ - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.WHITE_HERB, 0), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }); - trainerModifierPool[ModifierTier.ROGUE] = [ - new WeightedModifierType(modifierTypes.FOCUS_BAND, 2), - new WeightedModifierType(modifierTypes.LUCKY_EGG, 4), - new WeightedModifierType(modifierTypes.QUICK_CLAW, 1), - new WeightedModifierType(modifierTypes.GRIP_CLAW, 1), - new WeightedModifierType(modifierTypes.WIDE_LENS, 1), - ].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }); - trainerModifierPool[ModifierTier.MASTER] = [ - new WeightedModifierType(modifierTypes.KINGS_ROCK, 1), - new WeightedModifierType(modifierTypes.LEFTOVERS, 1), - new WeightedModifierType(modifierTypes.SHELL_BELL, 1), - new WeightedModifierType(modifierTypes.SCOPE_LENS, 1), - ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }); -} - -/** - * Initialize the enemy buff modifier pool - */ -function initEnemyBuffModifierPool() { - enemyBuffModifierPool[ModifierTier.COMMON] = [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 9), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 9), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_POISON_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_PARALYZE_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_BURN_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 9), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 4), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }); - enemyBuffModifierPool[ModifierTier.GREAT] = [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 5), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 5), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 5), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 5), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), - ].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }); - enemyBuffModifierPool[ModifierTier.ULTRA] = [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 10), - new WeightedModifierType(modifierTypes.ENEMY_HEAL, 10), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 10), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 10), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 5), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }); - enemyBuffModifierPool[ModifierTier.ROGUE] = [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.ROGUE); - return m; - }); - enemyBuffModifierPool[ModifierTier.MASTER] = [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.MASTER); - return m; - }); -} - -/** - * Initialize the daily starter modifier pool - */ -function initDailyStarterModifierPool() { - dailyStarterModifierPool[ModifierTier.COMMON] = [ - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1), - new WeightedModifierType(modifierTypes.BERRY, 3), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }); - dailyStarterModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 5)].map( - m => { - m.setTier(ModifierTier.GREAT); - return m; - }, - ); - dailyStarterModifierPool[ModifierTier.ULTRA] = [ - new WeightedModifierType(modifierTypes.REVIVER_SEED, 4), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, 1), - new WeightedModifierType(modifierTypes.SOUL_DEW, 1), - new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, 1), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }); - dailyStarterModifierPool[ModifierTier.ROGUE] = [ - new WeightedModifierType(modifierTypes.GRIP_CLAW, 5), - new WeightedModifierType(modifierTypes.BATON, 2), - new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), - new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), - new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), - ].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }); - dailyStarterModifierPool[ModifierTier.MASTER] = [ - new WeightedModifierType(modifierTypes.LEFTOVERS, 1), - new WeightedModifierType(modifierTypes.SHELL_BELL, 1), - ].map(m => { - m.setTier(ModifierTier.MASTER); + m.setTier(RewardTier.MASTER); return m; }); } @@ -793,12 +618,6 @@ export function initModifierPools() { initUltraModifierPool(); initRogueModifierPool(); initMasterModifierPool(); - - // Modifier pools for specific scenarios - initWildModifierPool(); - initTrainerModifierPool(); - initEnemyBuffModifierPool(); - initDailyStarterModifierPool(); } /** @@ -829,16 +648,15 @@ function skipInLastClassicWaveOrDefault(defaultWeight: number): WeightedModifier /** * High order function that returns a WeightedModifierTypeWeightFunc to ensure Lures don't spawn on Classic 199 * or if the lure still has over 60% of its duration left - * @param maxBattles The max battles the lure type in question lasts. 10 for green, 15 for Super, 30 for Max + * @param lureId The id of the lure type in question. * @param weight The desired weight for the lure when it does spawn * @returns A WeightedModifierTypeWeightFunc */ -function lureWeightFunc(maxBattles: number, weight: number): WeightedModifierTypeWeightFunc { +function lureWeightFunc(lureId: TrainerItemId, weight: number): WeightedModifierTypeWeightFunc { return () => { - const lures = globalScene.getModifiers(DoubleBattleChanceBoosterModifier); + const lureCount = globalScene.trainerItems.getStack(lureId); return !(globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex === 199) && - (lures.length === 0 || - lures.filter(m => m.getMaxBattles() === maxBattles && m.getBattleCount() >= maxBattles * 0.6).length === 0) + lureCount < allTrainerItems[lureId].getMaxStackCount() * 0.6 ? weight : 0; }; diff --git a/src/modifier/modifier-pools.ts b/src/modifier/modifier-pools.ts index 3396dca1f93..b0477f3c02f 100644 --- a/src/modifier/modifier-pools.ts +++ b/src/modifier/modifier-pools.ts @@ -6,11 +6,3 @@ import type { ModifierPool } from "#app/@types/modifier-types"; export const modifierPool: ModifierPool = {}; - -export const wildModifierPool: ModifierPool = {}; - -export const trainerModifierPool: ModifierPool = {}; - -export const enemyBuffModifierPool: ModifierPool = {}; - -export const dailyStarterModifierPool: ModifierPool = {}; diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 96aea699eff..6bf1c517795 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1,103 +1,37 @@ import { globalScene } from "#app/global-scene"; import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms"; -import { getBerryEffectDescription, getBerryName } from "#app/data/berry"; import { allMoves, modifierTypes } from "#app/data/data-lists"; import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; import { getPokeballCatchMultiplier, getPokeballName } from "#app/data/pokeball"; import { pokemonFormChanges, SpeciesFormChangeCondition } from "#app/data/pokemon-forms"; import { SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { FormChangeItem } from "#enums/form-change-item"; -import { getStatusEffectDescriptor } from "#app/data/status-effect"; +import { formChangeItemName } from "#app/data/pokemon-forms"; import { PokemonType } from "#enums/pokemon-type"; -import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import type { PlayerPokemon } from "#app/field/pokemon"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { AddPokeballModifier, AddVoucherModifier, - AttackTypeBoosterModifier, - BaseStatModifier, - BerryModifier, - BoostBugSpawnModifier, - BypassSpeedChanceModifier, - ContactHeldItemTransferChanceModifier, - CritBoosterModifier, - DamageMoneyRewardModifier, - DoubleBattleChanceBoosterModifier, - EnemyAttackStatusEffectChanceModifier, - EnemyDamageBoosterModifier, - EnemyDamageReducerModifier, - EnemyEndureChanceModifier, - EnemyFusionChanceModifier, - EnemyStatusEffectHealChanceModifier, - EnemyTurnHealModifier, EvolutionItemModifier, - EvolutionStatBoosterModifier, - EvoTrackerModifier, - ExpBalanceModifier, - ExpBoosterModifier, - ExpShareModifier, - ExtraModifierModifier, - FlinchChanceModifier, FusePokemonModifier, - GigantamaxAccessModifier, - HealingBoosterModifier, - HealShopCostModifier, - HiddenAbilityRateBoosterModifier, - HitHealModifier, - IvScannerModifier, - LevelIncrementBoosterModifier, - LockModifierTiersModifier, - MapModifier, - MegaEvolutionAccessModifier, - MoneyInterestModifier, - MoneyMultiplierModifier, - MoneyRewardModifier, - MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, - PokemonBaseStatFlatModifier, - PokemonBaseStatTotalModifier, - PokemonExpBoosterModifier, - PokemonFormChangeItemModifier, - PokemonFriendshipBoosterModifier, - PokemonHeldItemModifier, PokemonHpRestoreModifier, - PokemonIncrementingStatModifier, - PokemonInstantReviveModifier, PokemonLevelIncrementModifier, - PokemonMoveAccuracyBoosterModifier, - PokemonMultiHitModifier, PokemonNatureChangeModifier, - PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, - PreserveBerryModifier, RememberMoveModifier, - ResetNegativeStatStageModifier, - ShinyRateBoosterModifier, - SpeciesCritBoosterModifier, - SpeciesStatBoosterModifier, - SurviveDamageModifier, - SwitchEffectTransferModifier, - TempCritBoosterModifier, - TempStatStageBoosterModifier, - TerastallizeAccessModifier, TerrastalizeModifier, TmModifier, - TurnHealModifier, - TurnHeldItemTransferModifier, - TurnStatusEffectModifier, - type EnemyPersistentModifier, type Modifier, - type PersistentModifier, - TempExtraModifierModifier, - CriticalCatchChanceBoosterModifier, - FieldEffectModifier, + MoneyRewardModifier, } from "#app/modifier/modifier"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import Overrides from "#app/overrides"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; @@ -120,19 +54,29 @@ import { PokeballType } from "#enums/pokeball"; import { SpeciesId } from "#enums/species-id"; import { SpeciesFormKey } from "#enums/species-form-key"; import type { PermanentStat, TempBattleStat } from "#enums/stat"; -import { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; -import { StatusEffect } from "#enums/status-effect"; +import { Stat, TEMP_BATTLE_STATS } from "#enums/stat"; import i18next from "i18next"; import { timedEventManager } from "#app/global-event-manager"; +import { HeldItemId } from "#enums/held-item-id"; +import { allHeldItems } from "#app/data/data-lists"; import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; +import { attackTypeToHeldItem } from "#app/items/held-items/attack-type-booster"; +import { permanentStatToHeldItem, statBoostItems } from "#app/items/held-items/base-stat-booster"; +import { + SPECIES_STAT_BOOSTER_ITEMS, + type SpeciesStatBoostHeldItem, + type SpeciesStatBoosterItemId, +} from "#app/items/held-items/stat-booster"; import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { getModifierPoolForType, getModifierType } from "#app/utils/modifier-utils"; +import { getModifierPoolForType } from "#app/utils/modifier-utils"; import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types"; +import { getNewAttackTypeBoosterHeldItem, getNewBerryHeldItem, getNewVitaminHeldItem } from "#app/items/held-item-pool"; +import { berryTypeToHeldItem } from "#app/items/held-items/berry"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { allTrainerItems } from "#app/data/data-lists"; +import { tempStatToTrainerItem, TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; -const outputModifierData = false; -const useMaxWeightForOutput = false; - -type NewModifierFunc = (type: ModifierType, args: any[]) => Modifier; +type NewModifierFunc = (type: ModifierType, args: any[]) => Modifier | null; export class ModifierType { public id: string; @@ -140,7 +84,7 @@ export class ModifierType { public iconImage: string; public group: string; public soundName: string; - public tier: ModifierTier; + public tier: RewardTier; protected newModifierFunc: NewModifierFunc | null; /** @@ -178,45 +122,12 @@ export class ModifierType { return i18next.t(`${this.localeKey}.description` as any); } - setTier(tier: ModifierTier): void { - this.tier = tier; + getIcon(): string { + return this.iconImage; } - getOrInferTier(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierTier | null { - if (this.tier) { - return this.tier; - } - if (!this.id) { - return null; - } - let poolTypes: ModifierPoolType[]; - switch (poolType) { - case ModifierPoolType.PLAYER: - poolTypes = [poolType, ModifierPoolType.TRAINER, ModifierPoolType.WILD]; - break; - case ModifierPoolType.WILD: - poolTypes = [poolType, ModifierPoolType.PLAYER, ModifierPoolType.TRAINER]; - break; - case ModifierPoolType.TRAINER: - poolTypes = [poolType, ModifierPoolType.PLAYER, ModifierPoolType.WILD]; - break; - default: - poolTypes = [poolType]; - break; - } - // Try multiple pool types in case of stolen items - for (const type of poolTypes) { - const pool = getModifierPoolForType(type); - for (const tier of getEnumValues(ModifierTier)) { - if (!pool.hasOwnProperty(tier)) { - continue; - } - if (pool[tier].find(m => (m as WeightedModifierType).modifierType.id === this.id)) { - return (this.tier = tier); - } - } - } - return null; + setTier(tier: RewardTier): void { + this.tier = tier; } /** @@ -245,7 +156,7 @@ export class ModifierType { party?: PlayerPokemon[], rerollCount = 0, ): ModifierType { - let defaultTier: undefined | ModifierTier; + let defaultTier: undefined | RewardTier; for (const tier of Object.values(getModifierPoolForType(poolType))) { for (const modifier of tier) { if (this.id === modifier.modifierType.id) { @@ -382,30 +293,22 @@ export class PokemonModifierType extends ModifierType { } } -export class PokemonHeldItemModifierType extends PokemonModifierType { - constructor( - localeKey: string, - iconImage: string, - newModifierFunc: NewModifierFunc, - group?: string, - soundName?: string, - ) { +export class HeldItemReward extends PokemonModifierType { + public itemId: HeldItemId; + constructor(itemId: HeldItemId, group?: string, soundName?: string) { super( - localeKey, - iconImage, - newModifierFunc, + "", + "", + () => null, (pokemon: PlayerPokemon) => { - const dummyModifier = this.newModifier(pokemon); - const matchingModifier = globalScene.findModifier( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id && m.matchType(dummyModifier), - ) as PokemonHeldItemModifier; - const maxStackCount = dummyModifier.getMaxStackCount(); + const hasItem = pokemon.heldItemManager.hasItem(this.itemId); + const maxStackCount = allHeldItems[this.itemId].getMaxStackCount(); if (!maxStackCount) { return i18next.t("modifierType:ModifierType.PokemonHeldItemModifierType.extra.inoperable", { pokemonName: getPokemonNameWithAffix(pokemon), }); } - if (matchingModifier && matchingModifier.stackCount === maxStackCount) { + if (hasItem && pokemon.heldItemManager.getStack(this.itemId) === maxStackCount) { return i18next.t("modifierType:ModifierType.PokemonHeldItemModifierType.extra.tooMany", { pokemonName: getPokemonNameWithAffix(pokemon), }); @@ -415,10 +318,54 @@ export class PokemonHeldItemModifierType extends PokemonModifierType { group, soundName, ); + this.itemId = itemId; } - newModifier(...args: any[]): PokemonHeldItemModifier { - return super.newModifier(...args) as PokemonHeldItemModifier; + get name(): string { + return allHeldItems[this.itemId].name; + } + + getDescription(): string { + return allHeldItems[this.itemId].description; + } + + getIcon(): string { + return allHeldItems[this.itemId].iconName; + } + + apply(pokemon: Pokemon) { + pokemon.heldItemManager.add(this.itemId); + } +} + +export class TrainerItemReward extends ModifierType { + public itemId: TrainerItemId; + constructor(itemId: TrainerItemId, group?: string, soundName?: string) { + super("", "", () => null, group, soundName); + this.itemId = itemId; + } + + get name(): string { + return allTrainerItems[this.itemId].name; + } + + getDescription(): string { + return allTrainerItems[this.itemId].description; + } + + getIcon(): string { + return allTrainerItems[this.itemId].iconName; + } + + apply() { + globalScene.trainerItems.add(this.itemId); + } +} + +export class LapsingTrainerItemReward extends TrainerItemReward { + apply() { + globalScene.trainerItems.add(this.itemId, allTrainerItems[this.itemId].getMaxStackCount()); + console.log("WE GOT HERE WE ADDED IT"); } } @@ -736,165 +683,35 @@ export class RememberMoveModifierType extends PokemonModifierType { } } -export class DoubleBattleChanceBoosterModifierType extends ModifierType { - private maxBattles: number; - - constructor(localeKey: string, iconImage: string, maxBattles: number) { - super(localeKey, iconImage, (_type, _args) => new DoubleBattleChanceBoosterModifier(this, maxBattles), "lure"); - - this.maxBattles = maxBattles; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", { - battleCount: this.maxBattles, +class BerryRewardGenerator extends ModifierTypeGenerator { + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in BerryType) { + const item = berryTypeToHeldItem[pregenArgs[0] as BerryType]; + return new HeldItemReward(item); + } + const item = getNewBerryHeldItem(); + return new HeldItemReward(item); }); } } -export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType { - private stat: TempBattleStat; - private nameKey: string; - private quantityKey: string; - - constructor(stat: TempBattleStat) { - const nameKey = TempStatStageBoosterModifierTypeGenerator.items[stat]; - super("", nameKey, (_type, _args) => new TempStatStageBoosterModifier(this, this.stat, 5)); - - this.stat = stat; - this.nameKey = nameKey; - this.quantityKey = stat !== Stat.ACC ? "percentage" : "stage"; - } - - get name(): string { - return i18next.t(`modifierType:TempStatStageBoosterItem.${this.nameKey}`); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { - stat: i18next.t(getStatKey(this.stat)), - amount: i18next.t(`modifierType:ModifierType.TempStatStageBoosterModifierType.extra.${this.quantityKey}`), - }); - } - - getPregenArgs(): any[] { - return [this.stat]; - } -} - -export class BerryModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { - private berryType: BerryType; - - constructor(berryType: BerryType) { - super( - "", - `${BerryType[berryType].toLowerCase()}_berry`, - (type, args) => new BerryModifier(type, (args[0] as Pokemon).id, berryType), - "berry", - ); - - this.berryType = berryType; - this.id = "BERRY"; // needed to prevent harvest item deletion; remove after modifier rework - } - - get name(): string { - return getBerryName(this.berryType); - } - - getDescription(): string { - return getBerryEffectDescription(this.berryType); - } - - getPregenArgs(): any[] { - return [this.berryType]; - } -} - -enum AttackTypeBoosterItem { - SILK_SCARF, - BLACK_BELT, - SHARP_BEAK, - POISON_BARB, - SOFT_SAND, - HARD_STONE, - SILVER_POWDER, - SPELL_TAG, - METAL_COAT, - CHARCOAL, - MYSTIC_WATER, - MIRACLE_SEED, - MAGNET, - TWISTED_SPOON, - NEVER_MELT_ICE, - DRAGON_FANG, - BLACK_GLASSES, - FAIRY_FEATHER, -} - -export class AttackTypeBoosterModifierType - extends PokemonHeldItemModifierType - implements GeneratedPersistentModifierType -{ +export class AttackTypeBoosterReward extends HeldItemReward implements GeneratedPersistentModifierType { public moveType: PokemonType; public boostPercent: number; constructor(moveType: PokemonType, boostPercent: number) { - super( - "", - `${AttackTypeBoosterItem[moveType]?.toLowerCase()}`, - (_type, args) => new AttackTypeBoosterModifier(this, (args[0] as Pokemon).id, moveType, boostPercent), - ); - + const itemId = attackTypeToHeldItem[moveType]; + super(itemId); this.moveType = moveType; this.boostPercent = boostPercent; } - get name(): string { - return i18next.t(`modifierType:AttackTypeBoosterItem.${AttackTypeBoosterItem[this.moveType]?.toLowerCase()}`); - } - - getDescription(): string { - // TODO: Need getTypeName? - return i18next.t("modifierType:ModifierType.AttackTypeBoosterModifierType.description", { - moveType: i18next.t(`pokemonInfo:Type.${PokemonType[this.moveType]}`), - }); - } - getPregenArgs(): any[] { return [this.moveType]; } } -export type SpeciesStatBoosterItem = keyof typeof SpeciesStatBoosterModifierTypeGenerator.items; - -/** - * Modifier type for {@linkcode SpeciesStatBoosterModifier} - * @extends PokemonHeldItemModifierType - * @implements GeneratedPersistentModifierType - */ -export class SpeciesStatBoosterModifierType - extends PokemonHeldItemModifierType - implements GeneratedPersistentModifierType -{ - public key: SpeciesStatBoosterItem; - - constructor(key: SpeciesStatBoosterItem) { - const item = SpeciesStatBoosterModifierTypeGenerator.items[key]; - super( - `modifierType:SpeciesBoosterItem.${key}`, - key.toLowerCase(), - (type, args) => - new SpeciesStatBoosterModifier(type, (args[0] as Pokemon).id, item.stats, item.multiplier, item.species), - ); - - this.key = key; - } - - getPregenArgs(): any[] { - return [this.key]; - } -} - export class PokemonLevelIncrementModifierType extends PokemonModifierType { constructor(localeKey: string, iconImage: string) { super( @@ -907,10 +724,8 @@ export class PokemonLevelIncrementModifierType extends PokemonModifierType { getDescription(): string { let levels = 1; - const hasCandyJar = globalScene.modifiers.find(modifier => modifier instanceof LevelIncrementBoosterModifier); - if (hasCandyJar) { - levels += hasCandyJar.stackCount; - } + const candyJarStack = globalScene.trainerItems.getStack(TrainerItemId.CANDY_JAR); + levels += candyJarStack; return i18next.t("modifierType:ModifierType.PokemonLevelIncrementModifierType.description", { levels }); } } @@ -922,72 +737,40 @@ export class AllPokemonLevelIncrementModifierType extends ModifierType { getDescription(): string { let levels = 1; - const hasCandyJar = globalScene.modifiers.find(modifier => modifier instanceof LevelIncrementBoosterModifier); - if (hasCandyJar) { - levels += hasCandyJar.stackCount; - } + const candyJarStack = globalScene.trainerItems.getStack(TrainerItemId.CANDY_JAR); + levels += candyJarStack; return i18next.t("modifierType:ModifierType.AllPokemonLevelIncrementModifierType.description", { levels }); } } -export class BaseStatBoosterModifierType - extends PokemonHeldItemModifierType - implements GeneratedPersistentModifierType -{ +export class BaseStatBoosterReward extends HeldItemReward { private stat: PermanentStat; private key: string; constructor(stat: PermanentStat) { - const key = BaseStatBoosterModifierTypeGenerator.items[stat]; - super("", key, (_type, args) => new BaseStatModifier(this, (args[0] as Pokemon).id, this.stat)); + const key = statBoostItems[stat]; + const itemId = permanentStatToHeldItem[stat]; + super(itemId); this.stat = stat; this.key = key; } - - get name(): string { - return i18next.t(`modifierType:BaseStatBoosterItem.${this.key}`); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", { - stat: i18next.t(getStatKey(this.stat)), - }); - } - - getPregenArgs(): any[] { - return [this.stat]; - } } /** * Shuckle Juice item */ -export class PokemonBaseStatTotalModifierType - extends PokemonHeldItemModifierType - implements GeneratedPersistentModifierType -{ - private readonly statModifier: 10 | -15; +export class BaseStatTotalHeldItemReward extends HeldItemReward { + private readonly statModifier: number; - constructor(statModifier: 10 | -15) { - super( - statModifier > 0 - ? "modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD" - : "modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD", - statModifier > 0 ? "berry_juice_good" : "berry_juice_bad", - (_type, args) => new PokemonBaseStatTotalModifier(this, (args[0] as Pokemon).id, statModifier), - ); + constructor(itemId: HeldItemId, statModifier: number) { + super(itemId); this.statModifier = statModifier; } - override getDescription(): string { - return this.statModifier > 0 - ? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.description") - : i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.description"); - } - - public getPregenArgs(): any[] { - return [this.statModifier]; + apply(pokemon: Pokemon) { + super.apply(pokemon); + pokemon.heldItemManager[this.itemId].data.statModifier = this.statModifier; } } @@ -1035,7 +818,7 @@ export class MoneyRewardModifierType extends ModifierType { getDescription(): string { const moneyAmount = new NumberHolder(globalScene.getWaveMoneyAmount(this.moneyMultiplier)); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); const formattedMoney = formatMoney(globalScene.moneyFormat, moneyAmount.value); return i18next.t("modifierType:ModifierType.MoneyRewardModifierType.description", { @@ -1045,88 +828,6 @@ export class MoneyRewardModifierType extends ModifierType { } } -export class ExpBoosterModifierType extends ModifierType { - private boostPercent: number; - - constructor(localeKey: string, iconImage: string, boostPercent: number) { - super(localeKey, iconImage, () => new ExpBoosterModifier(this, boostPercent)); - - this.boostPercent = boostPercent; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.ExpBoosterModifierType.description", { - boostPercent: this.boostPercent, - }); - } -} - -export class PokemonExpBoosterModifierType extends PokemonHeldItemModifierType { - private boostPercent: number; - - constructor(localeKey: string, iconImage: string, boostPercent: number) { - super( - localeKey, - iconImage, - (_type, args) => new PokemonExpBoosterModifier(this, (args[0] as Pokemon).id, boostPercent), - ); - - this.boostPercent = boostPercent; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonExpBoosterModifierType.description", { - boostPercent: this.boostPercent, - }); - } -} - -export class PokemonFriendshipBoosterModifierType extends PokemonHeldItemModifierType { - constructor(localeKey: string, iconImage: string) { - super(localeKey, iconImage, (_type, args) => new PokemonFriendshipBoosterModifier(this, (args[0] as Pokemon).id)); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonFriendshipBoosterModifierType.description"); - } -} - -export class PokemonMoveAccuracyBoosterModifierType extends PokemonHeldItemModifierType { - private amount: number; - - constructor(localeKey: string, iconImage: string, amount: number, group?: string, soundName?: string) { - super( - localeKey, - iconImage, - (_type, args) => new PokemonMoveAccuracyBoosterModifier(this, (args[0] as Pokemon).id, amount), - group, - soundName, - ); - - this.amount = amount; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonMoveAccuracyBoosterModifierType.description", { - accuracyAmount: this.amount, - }); - } -} - -export class PokemonMultiHitModifierType extends PokemonHeldItemModifierType { - constructor(localeKey: string, iconImage: string) { - super( - localeKey, - iconImage, - (type, args) => new PokemonMultiHitModifier(type as PokemonMultiHitModifierType, (args[0] as Pokemon).id), - ); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonMultiHitModifierType.description"); - } -} - export class TmModifierType extends PokemonModifierType { public moveId: MoveId; @@ -1218,14 +919,14 @@ export class EvolutionItemModifierType extends PokemonModifierType implements Ge /** * Class that represents form changing items */ -export class FormChangeItemModifierType extends PokemonModifierType implements GeneratedPersistentModifierType { +export class FormChangeItemReward extends PokemonModifierType { public formChangeItem: FormChangeItem; constructor(formChangeItem: FormChangeItem) { super( "", FormChangeItem[formChangeItem].toLowerCase(), - (_type, args) => new PokemonFormChangeItemModifier(this, (args[0] as PlayerPokemon).id, formChangeItem, true), + () => null, (pokemon: PlayerPokemon) => { // Make sure the Pokemon has alternate forms if ( @@ -1251,15 +952,24 @@ export class FormChangeItemModifierType extends PokemonModifierType implements G } get name(): string { - return i18next.t(`modifierType:FormChangeItem.${FormChangeItem[this.formChangeItem]}`); + return formChangeItemName(this.formChangeItem); } getDescription(): string { return i18next.t("modifierType:ModifierType.FormChangeItemModifierType.description"); } - getPregenArgs(): any[] { - return [this.formChangeItem]; + apply(pokemon: Pokemon) { + if (pokemon.heldItemManager.hasFormChangeItem(this.formChangeItem)) { + return; + } + + pokemon.heldItemManager.addFormChangeItem(this.formChangeItem); + pokemon.heldItemManager.toggleActive(this.formChangeItem); + + globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger); + + globalScene.updateItems(true); } } @@ -1283,85 +993,32 @@ export class FusePokemonModifierType extends PokemonModifierType { } } -class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { +class AttackTypeBoosterRewardGenerator extends ModifierTypeGenerator { constructor() { super((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) { - return new AttackTypeBoosterModifierType(pregenArgs[0] as PokemonType, TYPE_BOOST_ITEM_BOOST_PERCENT); + return new AttackTypeBoosterReward(pregenArgs[0] as PokemonType, TYPE_BOOST_ITEM_BOOST_PERCENT); } - const attackMoveTypes = party.flatMap(p => - p - .getMoveset() - .map(m => m.getMove()) - .filter(m => m.is("AttackMove")) - .map(m => m.type), - ); - if (!attackMoveTypes.length) { - return null; - } + const item = getNewAttackTypeBoosterHeldItem(party); - const attackMoveTypeWeights = new Map(); - let totalWeight = 0; - for (const t of attackMoveTypes) { - if (attackMoveTypeWeights.has(t)) { - if (attackMoveTypeWeights.get(t)! < 3) { - // attackMoveTypeWeights.has(t) was checked before - attackMoveTypeWeights.set(t, attackMoveTypeWeights.get(t)! + 1); - } else { - continue; - } - } else { - attackMoveTypeWeights.set(t, 1); - } - totalWeight++; - } - - if (!totalWeight) { - return null; - } - - let type: PokemonType; - - const randInt = randSeedInt(totalWeight); - let weight = 0; - - for (const t of attackMoveTypeWeights.keys()) { - const typeWeight = attackMoveTypeWeights.get(t)!; // guranteed to be defined - if (randInt <= weight + typeWeight) { - type = t; - break; - } - weight += typeWeight; - } - - return new AttackTypeBoosterModifierType(type!, TYPE_BOOST_ITEM_BOOST_PERCENT); + return item ? new HeldItemReward(item) : null; }); } } -class BaseStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { - public static readonly items: Record = { - [Stat.HP]: "hp_up", - [Stat.ATK]: "protein", - [Stat.DEF]: "iron", - [Stat.SPATK]: "calcium", - [Stat.SPDEF]: "zinc", - [Stat.SPD]: "carbos", - }; - +class BaseStatBoosterRewardGenerator extends ModifierTypeGenerator { constructor() { super((_party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs) { - return new BaseStatBoosterModifierType(pregenArgs[0]); + return new BaseStatBoosterReward(pregenArgs[0]); } - const randStat: PermanentStat = randSeedInt(Stat.SPD + 1); - return new BaseStatBoosterModifierType(randStat); + return new HeldItemReward(getNewVitaminHeldItem()); }); } } -class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator { +class TempStatStageBoosterRewardGenerator extends ModifierTypeGenerator { public static readonly items: Record = { [Stat.ATK]: "x_attack", [Stat.DEF]: "x_defense", @@ -1374,10 +1031,10 @@ class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator { constructor() { super((_party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs && pregenArgs.length === 1 && TEMP_BATTLE_STATS.includes(pregenArgs[0])) { - return new TempStatStageBoosterModifierType(pregenArgs[0]); + return new LapsingTrainerItemReward(tempStatToTrainerItem[pregenArgs[0]]); } const randStat: TempBattleStat = randSeedInt(Stat.ACC, Stat.ATK); - return new TempStatStageBoosterModifierType(randStat); + return new LapsingTrainerItemReward(tempStatToTrainerItem[randStat]); }); } } @@ -1388,66 +1045,21 @@ class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator { * the current list of {@linkcode items}. * @extends ModifierTypeGenerator */ -class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { +class SpeciesStatBoosterRewardGenerator extends ModifierTypeGenerator { /** Object comprised of the currently available species-based stat boosting held items */ - public static readonly items = { - LIGHT_BALL: { - stats: [Stat.ATK, Stat.SPATK], - multiplier: 2, - species: [SpeciesId.PIKACHU], - rare: true, - }, - THICK_CLUB: { - stats: [Stat.ATK], - multiplier: 2, - species: [SpeciesId.CUBONE, SpeciesId.MAROWAK, SpeciesId.ALOLA_MAROWAK], - rare: true, - }, - METAL_POWDER: { - stats: [Stat.DEF], - multiplier: 2, - species: [SpeciesId.DITTO], - rare: true, - }, - QUICK_POWDER: { - stats: [Stat.SPD], - multiplier: 2, - species: [SpeciesId.DITTO], - rare: true, - }, - DEEP_SEA_SCALE: { - stats: [Stat.SPDEF], - multiplier: 2, - species: [SpeciesId.CLAMPERL], - rare: false, - }, - DEEP_SEA_TOOTH: { - stats: [Stat.SPATK], - multiplier: 2, - species: [SpeciesId.CLAMPERL], - rare: false, - }, - }; constructor(rare: boolean) { super((party: Pokemon[], pregenArgs?: any[]) => { - const items = SpeciesStatBoosterModifierTypeGenerator.items; - if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in items) { - return new SpeciesStatBoosterModifierType(pregenArgs[0] as SpeciesStatBoosterItem); + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in SPECIES_STAT_BOOSTER_ITEMS) { + return new HeldItemReward(pregenArgs[0] as HeldItemId); } // Get a pool of items based on the rarity. - const keys: (keyof SpeciesStatBoosterItem)[] = []; - const values: (typeof items)[keyof typeof items][] = []; - const weights: number[] = []; - for (const [key, val] of Object.entries(SpeciesStatBoosterModifierTypeGenerator.items)) { - if (val.rare !== rare) { - continue; - } - values.push(val); - keys.push(key as keyof SpeciesStatBoosterItem); - weights.push(0); - } + const tierItems = rare + ? [HeldItemId.LIGHT_BALL, HeldItemId.THICK_CLUB, HeldItemId.METAL_POWDER, HeldItemId.QUICK_POWDER] + : [HeldItemId.DEEP_SEA_SCALE, HeldItemId.DEEP_SEA_TOOTH]; + + const weights = new Array(tierItems.length).fill(0); for (const p of party) { const speciesId = p.getSpeciesForm(true).speciesId; @@ -1455,18 +1067,11 @@ class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { // TODO: Use commented boolean when Fling is implemented const hasFling = false; /* p.getMoveset(true).some(m => m.moveId === MoveId.FLING) */ - for (const i in values) { - const checkedSpecies = values[i].species; - const checkedStats = values[i].stats; + for (const i in tierItems) { + const checkedSpecies = (allHeldItems[tierItems[i]] as SpeciesStatBoostHeldItem).species; // If party member already has the item being weighted currently, skip to the next item - const hasItem = p - .getHeldItems() - .some( - m => - m instanceof SpeciesStatBoosterModifier && - (m as SpeciesStatBoosterModifier).contains(checkedSpecies[0], checkedStats[0]), - ); + const hasItem = p.heldItemManager.hasItem(tierItems[i]); if (!hasItem) { if (checkedSpecies.includes(speciesId) || (!!fusionSpeciesId && checkedSpecies.includes(fusionSpeciesId))) { @@ -1480,6 +1085,7 @@ class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { } } + // TODO: Replace this with a helper function let totalWeight = 0; for (const weight of weights) { totalWeight += weight; @@ -1493,7 +1099,7 @@ class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { if (weights[i] !== 0) { const curWeight = weight + weights[i]; if (randInt <= weight + weights[i]) { - return new SpeciesStatBoosterModifierType(keys[i] as SpeciesStatBoosterItem); + return new HeldItemReward(tierItems[i]); } weight = curWeight; } @@ -1506,7 +1112,7 @@ class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { } class TmModifierTypeGenerator extends ModifierTypeGenerator { - constructor(tier: ModifierTier) { + constructor(tier: RewardTier) { super((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in MoveId) { return new TmModifierType(pregenArgs[0] as MoveId); @@ -1583,11 +1189,11 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { } } -export class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { +export class FormChangeItemRewardGenerator extends ModifierTypeGenerator { constructor(isRareFormChangeItem: boolean) { super((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in FormChangeItem) { - return new FormChangeItemModifierType(pregenArgs[0] as FormChangeItem); + return new FormChangeItemReward(pregenArgs[0] as FormChangeItem); } const formChangeItemPool = [ @@ -1601,26 +1207,17 @@ export class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { fc => ((fc.formKey.indexOf(SpeciesFormKey.MEGA) === -1 && fc.formKey.indexOf(SpeciesFormKey.PRIMAL) === -1) || - globalScene.getModifiers(MegaEvolutionAccessModifier).length) && + globalScene.trainerItems.hasItem(TrainerItemId.MEGA_BRACELET)) && ((fc.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) === -1 && fc.formKey.indexOf(SpeciesFormKey.ETERNAMAX) === -1) || - globalScene.getModifiers(GigantamaxAccessModifier).length) && + globalScene.trainerItems.hasItem(TrainerItemId.DYNAMAX_BAND)) && (!fc.conditions.length || fc.conditions.filter(cond => cond instanceof SpeciesFormChangeCondition && cond.predicate(p)) .length) && fc.preFormKey === p.getFormKey(), ) .map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) - .filter( - t => - t?.active && - !globalScene.findModifier( - m => - m instanceof PokemonFormChangeItemModifier && - m.pokemonId === p.id && - m.formChangeItem === t.item, - ), - ); + .filter(t => t?.active && !p.heldItemManager.hasFormChangeItem(t.item)); if (p.species.speciesId === SpeciesId.NECROZMA) { // technically we could use a simplified version and check for formChanges.length > 3, but in case any code changes later, this might break... @@ -1663,85 +1260,7 @@ export class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { return null; } - return new FormChangeItemModifierType(formChangeItemPool[randSeedInt(formChangeItemPool.length)]); - }); - } -} - -export class ContactHeldItemTransferChanceModifierType extends PokemonHeldItemModifierType { - private chancePercent: number; - - constructor(localeKey: string, iconImage: string, chancePercent: number, group?: string, soundName?: string) { - super( - localeKey, - iconImage, - (type, args) => new ContactHeldItemTransferChanceModifier(type, (args[0] as Pokemon).id, chancePercent), - group, - soundName, - ); - - this.chancePercent = chancePercent; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.ContactHeldItemTransferChanceModifierType.description", { - chancePercent: this.chancePercent, - }); - } -} - -export class TurnHeldItemTransferModifierType extends PokemonHeldItemModifierType { - constructor(localeKey: string, iconImage: string, group?: string, soundName?: string) { - super( - localeKey, - iconImage, - (type, args) => new TurnHeldItemTransferModifier(type, (args[0] as Pokemon).id), - group, - soundName, - ); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.TurnHeldItemTransferModifierType.description"); - } -} - -export class EnemyAttackStatusEffectChanceModifierType extends ModifierType { - private chancePercent: number; - private effect: StatusEffect; - - constructor(localeKey: string, iconImage: string, chancePercent: number, effect: StatusEffect, stackCount?: number) { - super( - localeKey, - iconImage, - (type, _args) => new EnemyAttackStatusEffectChanceModifier(type, effect, chancePercent, stackCount), - "enemy_status_chance", - ); - - this.chancePercent = chancePercent; - this.effect = effect; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.EnemyAttackStatusEffectChanceModifierType.description", { - chancePercent: this.chancePercent, - statusEffect: getStatusEffectDescriptor(this.effect), - }); - } -} - -export class EnemyEndureChanceModifierType extends ModifierType { - private chancePercent: number; - - constructor(localeKey: string, iconImage: string, chancePercent: number) { - super(localeKey, iconImage, (type, _args) => new EnemyEndureChanceModifier(type, chancePercent), "enemy_endure"); - - this.chancePercent = chancePercent; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.EnemyEndureChanceModifierType.description", { - chancePercent: this.chancePercent, + return new FormChangeItemReward(formChangeItemPool[randSeedInt(formChangeItemPool.length)]); }); } } @@ -1762,7 +1281,7 @@ export class WeightedModifierType { this.maxWeight = maxWeight || (!(weight instanceof Function) ? weight : 0); } - setTier(tier: ModifierTier) { + setTier(tier: RewardTier) { this.modifierType.setTier(tier); } } @@ -1778,7 +1297,7 @@ export type GeneratorModifierOverride = { } & ( | { name: keyof Pick; - type?: SpeciesStatBoosterItem; + type?: SpeciesStatBoosterItemId; } | { name: keyof Pick; @@ -1831,37 +1350,17 @@ const modifierTypeInitObj = Object.freeze({ EVOLUTION_ITEM: () => new EvolutionItemModifierTypeGenerator(false), RARE_EVOLUTION_ITEM: () => new EvolutionItemModifierTypeGenerator(true), - FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(false), - RARE_FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(true), - EVOLUTION_TRACKER_GIMMIGHOUL: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", - "relic_gold", - (type, args) => - new EvoTrackerModifier(type, (args[0] as Pokemon).id, SpeciesId.GIMMIGHOUL, 10, (args[1] as number) ?? 1), - ), + FORM_CHANGE_ITEM: () => new FormChangeItemRewardGenerator(false), + RARE_FORM_CHANGE_ITEM: () => new FormChangeItemRewardGenerator(true), - MEGA_BRACELET: () => - new ModifierType( - "modifierType:ModifierType.MEGA_BRACELET", - "mega_bracelet", - (type, _args) => new MegaEvolutionAccessModifier(type), - ), - DYNAMAX_BAND: () => - new ModifierType( - "modifierType:ModifierType.DYNAMAX_BAND", - "dynamax_band", - (type, _args) => new GigantamaxAccessModifier(type), - ), - TERA_ORB: () => - new ModifierType( - "modifierType:ModifierType.TERA_ORB", - "tera_orb", - (type, _args) => new TerastallizeAccessModifier(type), - ), + EVOLUTION_TRACKER_GIMMIGHOUL: () => new HeldItemReward(HeldItemId.GIMMIGHOUL_EVO_TRACKER), - MAP: () => new ModifierType("modifierType:ModifierType.MAP", "map", (type, _args) => new MapModifier(type)), + MEGA_BRACELET: () => new TrainerItemReward(TrainerItemId.MEGA_BRACELET), + DYNAMAX_BAND: () => new TrainerItemReward(TrainerItemId.DYNAMAX_BAND), + TERA_ORB: () => new TrainerItemReward(TrainerItemId.TERA_ORB), + + MAP: () => new TrainerItemReward(TrainerItemId.MAP), POTION: () => new PokemonHpRestoreModifierType("modifierType:ModifierType.POTION", "potion", 20, 10), SUPER_POTION: () => @@ -1879,18 +1378,9 @@ const modifierTypeInitObj = Object.freeze({ SACRED_ASH: () => new AllPokemonFullReviveModifierType("modifierType:ModifierType.SACRED_ASH", "sacred_ash"), - REVIVER_SEED: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.REVIVER_SEED", - "reviver_seed", - (type, args) => new PokemonInstantReviveModifier(type, (args[0] as Pokemon).id), - ), - WHITE_HERB: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.WHITE_HERB", - "white_herb", - (type, args) => new ResetNegativeStatStageModifier(type, (args[0] as Pokemon).id), - ), + REVIVER_SEED: () => new HeldItemReward(HeldItemId.REVIVER_SEED), + + WHITE_HERB: () => new HeldItemReward(HeldItemId.WHITE_HERB), ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.ETHER", "ether", 10), MAX_ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.MAX_ETHER", "max_ether", -1), @@ -1905,28 +1395,20 @@ const modifierTypeInitObj = Object.freeze({ SUPER_REPEL: () => new DoubleBattleChanceBoosterModifierType('Super Repel', 10), MAX_REPEL: () => new DoubleBattleChanceBoosterModifierType('Max Repel', 25),*/ - LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.LURE", "lure", 10), - SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 15), - MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 30), + LURE: () => new LapsingTrainerItemReward(TrainerItemId.LURE), + SUPER_LURE: () => new LapsingTrainerItemReward(TrainerItemId.SUPER_LURE), + MAX_LURE: () => new LapsingTrainerItemReward(TrainerItemId.MAX_LURE), - SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(false), - RARE_SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(true), + SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterRewardGenerator(false), + RARE_SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterRewardGenerator(true), - TEMP_STAT_STAGE_BOOSTER: () => new TempStatStageBoosterModifierTypeGenerator(), + TEMP_STAT_STAGE_BOOSTER: () => new TempStatStageBoosterRewardGenerator(), - DIRE_HIT: () => - new (class extends ModifierType { - getDescription(): string { - return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { - stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises"), - amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.stage"), - }); - } - })("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new TempCritBoosterModifier(type, 5)), + DIRE_HIT: () => new LapsingTrainerItemReward(TrainerItemId.DIRE_HIT), - BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(), + BASE_STAT_BOOSTER: () => new BaseStatBoosterRewardGenerator(), - ATTACK_TYPE_BOOSTER: () => new AttackTypeBoosterModifierTypeGenerator(), + ATTACK_TYPE_BOOSTER: () => new AttackTypeBoosterRewardGenerator(), MINT: () => new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { @@ -1936,19 +1418,14 @@ const modifierTypeInitObj = Object.freeze({ return new PokemonNatureChangeModifierType(randSeedInt(getEnumValues(Nature).length) as Nature); }), - MYSTICAL_ROCK: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.MYSTICAL_ROCK", - "mystical_rock", - (type, args) => new FieldEffectModifier(type, (args[0] as Pokemon).id), - ), + MYSTICAL_ROCK: () => new HeldItemReward(HeldItemId.MYSTICAL_ROCK), TERA_SHARD: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) { return new TerastallizeModifierType(pregenArgs[0] as PokemonType); } - if (!globalScene.getModifiers(TerastallizeAccessModifier).length) { + if (!globalScene.trainerItems.hasItem(TrainerItemId.TERA_ORB)) { return null; } const teraTypes: PokemonType[] = []; @@ -1970,89 +1447,33 @@ const modifierTypeInitObj = Object.freeze({ return new TerastallizeModifierType(shardType); }), - BERRY: () => - 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 BerryModifierType(randBerryType); - }), + BERRY: () => new BerryRewardGenerator(), - TM_COMMON: () => new TmModifierTypeGenerator(ModifierTier.COMMON), - TM_GREAT: () => new TmModifierTypeGenerator(ModifierTier.GREAT), - TM_ULTRA: () => new TmModifierTypeGenerator(ModifierTier.ULTRA), + TM_COMMON: () => new TmModifierTypeGenerator(RewardTier.COMMON), + TM_GREAT: () => new TmModifierTypeGenerator(RewardTier.GREAT), + TM_ULTRA: () => new TmModifierTypeGenerator(RewardTier.ULTRA), MEMORY_MUSHROOM: () => new RememberMoveModifierType("modifierType:ModifierType.MEMORY_MUSHROOM", "big_mushroom"), - EXP_SHARE: () => - new ModifierType("modifierType:ModifierType.EXP_SHARE", "exp_share", (type, _args) => new ExpShareModifier(type)), - EXP_BALANCE: () => - new ModifierType( - "modifierType:ModifierType.EXP_BALANCE", - "exp_balance", - (type, _args) => new ExpBalanceModifier(type), - ), + EXP_SHARE: () => new TrainerItemReward(TrainerItemId.EXP_SHARE), + EXP_BALANCE: () => new TrainerItemReward(TrainerItemId.EXP_BALANCE), - OVAL_CHARM: () => - new ModifierType( - "modifierType:ModifierType.OVAL_CHARM", - "oval_charm", - (type, _args) => new MultipleParticipantExpBonusModifier(type), - ), + OVAL_CHARM: () => new TrainerItemReward(TrainerItemId.OVAL_CHARM), - EXP_CHARM: () => new ExpBoosterModifierType("modifierType:ModifierType.EXP_CHARM", "exp_charm", 25), - SUPER_EXP_CHARM: () => new ExpBoosterModifierType("modifierType:ModifierType.SUPER_EXP_CHARM", "super_exp_charm", 60), - GOLDEN_EXP_CHARM: () => - new ExpBoosterModifierType("modifierType:ModifierType.GOLDEN_EXP_CHARM", "golden_exp_charm", 100), + EXP_CHARM: () => new TrainerItemReward(TrainerItemId.EXP_CHARM), + SUPER_EXP_CHARM: () => new TrainerItemReward(TrainerItemId.SUPER_EXP_CHARM), - LUCKY_EGG: () => new PokemonExpBoosterModifierType("modifierType:ModifierType.LUCKY_EGG", "lucky_egg", 40), - GOLDEN_EGG: () => new PokemonExpBoosterModifierType("modifierType:ModifierType.GOLDEN_EGG", "golden_egg", 100), + LUCKY_EGG: () => new HeldItemReward(HeldItemId.LUCKY_EGG), + GOLDEN_EGG: () => new HeldItemReward(HeldItemId.GOLDEN_EGG), - SOOTHE_BELL: () => new PokemonFriendshipBoosterModifierType("modifierType:ModifierType.SOOTHE_BELL", "soothe_bell"), + SOOTHE_BELL: () => new HeldItemReward(HeldItemId.SOOTHE_BELL), - SCOPE_LENS: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.SCOPE_LENS", - "scope_lens", - (type, args) => new CritBoosterModifier(type, (args[0] as Pokemon).id, 1), - ), - LEEK: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.LEEK", - "leek", - (type, args) => - new SpeciesCritBoosterModifier(type, (args[0] as Pokemon).id, 2, [ - SpeciesId.FARFETCHD, - SpeciesId.GALAR_FARFETCHD, - SpeciesId.SIRFETCHD, - ]), - ), + SCOPE_LENS: () => new HeldItemReward(HeldItemId.SCOPE_LENS), + LEEK: () => new HeldItemReward(HeldItemId.LEEK), - EVIOLITE: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.EVIOLITE", - "eviolite", - (type, args) => new EvolutionStatBoosterModifier(type, (args[0] as Pokemon).id, [Stat.DEF, Stat.SPDEF], 1.5), - ), + EVIOLITE: () => new HeldItemReward(HeldItemId.EVIOLITE), - SOUL_DEW: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.SOUL_DEW", - "soul_dew", - (type, args) => new PokemonNatureWeightModifier(type, (args[0] as Pokemon).id), - ), + SOUL_DEW: () => new HeldItemReward(HeldItemId.SOUL_DEW), NUGGET: () => new MoneyRewardModifierType( @@ -2076,258 +1497,74 @@ const modifierTypeInitObj = Object.freeze({ "modifierType:ModifierType.MoneyRewardModifierType.extra.large", ), - AMULET_COIN: () => - new ModifierType( - "modifierType:ModifierType.AMULET_COIN", - "amulet_coin", - (type, _args) => new MoneyMultiplierModifier(type), - ), - GOLDEN_PUNCH: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.GOLDEN_PUNCH", - "golden_punch", - (type, args) => new DamageMoneyRewardModifier(type, (args[0] as Pokemon).id), - ), - COIN_CASE: () => - new ModifierType( - "modifierType:ModifierType.COIN_CASE", - "coin_case", - (type, _args) => new MoneyInterestModifier(type), - ), + AMULET_COIN: () => new TrainerItemReward(TrainerItemId.AMULET_COIN), + GOLDEN_PUNCH: () => new HeldItemReward(HeldItemId.GOLDEN_PUNCH), - LOCK_CAPSULE: () => - new ModifierType( - "modifierType:ModifierType.LOCK_CAPSULE", - "lock_capsule", - (type, _args) => new LockModifierTiersModifier(type), - ), + LOCK_CAPSULE: () => new TrainerItemReward(TrainerItemId.LOCK_CAPSULE), - GRIP_CLAW: () => - new ContactHeldItemTransferChanceModifierType("modifierType:ModifierType.GRIP_CLAW", "grip_claw", 10), - WIDE_LENS: () => new PokemonMoveAccuracyBoosterModifierType("modifierType:ModifierType.WIDE_LENS", "wide_lens", 5), + GRIP_CLAW: () => new HeldItemReward(HeldItemId.GRIP_CLAW), + WIDE_LENS: () => new HeldItemReward(HeldItemId.WIDE_LENS), - MULTI_LENS: () => new PokemonMultiHitModifierType("modifierType:ModifierType.MULTI_LENS", "zoom_lens"), + MULTI_LENS: () => new HeldItemReward(HeldItemId.MULTI_LENS), - HEALING_CHARM: () => - new ModifierType( - "modifierType:ModifierType.HEALING_CHARM", - "healing_charm", - (type, _args) => new HealingBoosterModifier(type, 1.1), - ), - CANDY_JAR: () => - new ModifierType( - "modifierType:ModifierType.CANDY_JAR", - "candy_jar", - (type, _args) => new LevelIncrementBoosterModifier(type), - ), + HEALING_CHARM: () => new TrainerItemReward(TrainerItemId.HEALING_CHARM), + CANDY_JAR: () => new TrainerItemReward(TrainerItemId.CANDY_JAR), - BERRY_POUCH: () => - new ModifierType( - "modifierType:ModifierType.BERRY_POUCH", - "berry_pouch", - (type, _args) => new PreserveBerryModifier(type), - ), + BERRY_POUCH: () => new TrainerItemReward(TrainerItemId.BERRY_POUCH), - FOCUS_BAND: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.FOCUS_BAND", - "focus_band", - (type, args) => new SurviveDamageModifier(type, (args[0] as Pokemon).id), - ), + FOCUS_BAND: () => new HeldItemReward(HeldItemId.FOCUS_BAND), - QUICK_CLAW: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.QUICK_CLAW", - "quick_claw", - (type, args) => new BypassSpeedChanceModifier(type, (args[0] as Pokemon).id), - ), + QUICK_CLAW: () => new HeldItemReward(HeldItemId.QUICK_CLAW), - KINGS_ROCK: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.KINGS_ROCK", - "kings_rock", - (type, args) => new FlinchChanceModifier(type, (args[0] as Pokemon).id), - ), + KINGS_ROCK: () => new HeldItemReward(HeldItemId.KINGS_ROCK), - LEFTOVERS: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.LEFTOVERS", - "leftovers", - (type, args) => new TurnHealModifier(type, (args[0] as Pokemon).id), - ), - SHELL_BELL: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.SHELL_BELL", - "shell_bell", - (type, args) => new HitHealModifier(type, (args[0] as Pokemon).id), - ), + LEFTOVERS: () => new HeldItemReward(HeldItemId.LEFTOVERS), - TOXIC_ORB: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.TOXIC_ORB", - "toxic_orb", - (type, args) => new TurnStatusEffectModifier(type, (args[0] as Pokemon).id), - ), - FLAME_ORB: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.FLAME_ORB", - "flame_orb", - (type, args) => new TurnStatusEffectModifier(type, (args[0] as Pokemon).id), - ), + SHELL_BELL: () => new HeldItemReward(HeldItemId.SHELL_BELL), - BATON: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.BATON", - "baton", - (type, args) => new SwitchEffectTransferModifier(type, (args[0] as Pokemon).id), - ), + TOXIC_ORB: () => new HeldItemReward(HeldItemId.TOXIC_ORB), - SHINY_CHARM: () => - new ModifierType( - "modifierType:ModifierType.SHINY_CHARM", - "shiny_charm", - (type, _args) => new ShinyRateBoosterModifier(type), - ), - ABILITY_CHARM: () => - new ModifierType( - "modifierType:ModifierType.ABILITY_CHARM", - "ability_charm", - (type, _args) => new HiddenAbilityRateBoosterModifier(type), - ), - CATCHING_CHARM: () => - new ModifierType( - "modifierType:ModifierType.CATCHING_CHARM", - "catching_charm", - (type, _args) => new CriticalCatchChanceBoosterModifier(type), - ), + FLAME_ORB: () => new HeldItemReward(HeldItemId.FLAME_ORB), - IV_SCANNER: () => - new ModifierType("modifierType:ModifierType.IV_SCANNER", "scanner", (type, _args) => new IvScannerModifier(type)), + BATON: () => new HeldItemReward(HeldItemId.BATON), + + SHINY_CHARM: () => new TrainerItemReward(TrainerItemId.SHINY_CHARM), + ABILITY_CHARM: () => new TrainerItemReward(TrainerItemId.ABILITY_CHARM), + CATCHING_CHARM: () => new TrainerItemReward(TrainerItemId.CATCHING_CHARM), + + IV_SCANNER: () => new TrainerItemReward(TrainerItemId.IV_SCANNER), DNA_SPLICERS: () => new FusePokemonModifierType("modifierType:ModifierType.DNA_SPLICERS", "dna_splicers"), - MINI_BLACK_HOLE: () => - new TurnHeldItemTransferModifierType("modifierType:ModifierType.MINI_BLACK_HOLE", "mini_black_hole"), + MINI_BLACK_HOLE: () => new HeldItemReward(HeldItemId.MINI_BLACK_HOLE), VOUCHER: () => new AddVoucherModifierType(VoucherType.REGULAR, 1), VOUCHER_PLUS: () => new AddVoucherModifierType(VoucherType.PLUS, 1), VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1), - GOLDEN_POKEBALL: () => - new ModifierType( - "modifierType:ModifierType.GOLDEN_POKEBALL", - "pb_gold", - (type, _args) => new ExtraModifierModifier(type), - undefined, - "se/pb_bounce_1", - ), - SILVER_POKEBALL: () => - new ModifierType( - "modifierType:ModifierType.SILVER_POKEBALL", - "pb_silver", - (type, _args) => new TempExtraModifierModifier(type, 100), - undefined, - "se/pb_bounce_1", - ), + GOLDEN_POKEBALL: () => new TrainerItemReward(TrainerItemId.GOLDEN_POKEBALL), - ENEMY_DAMAGE_BOOSTER: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_DAMAGE_BOOSTER", - "wl_item_drop", - (type, _args) => new EnemyDamageBoosterModifier(type, 5), - ), - ENEMY_DAMAGE_REDUCTION: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_DAMAGE_REDUCTION", - "wl_guard_spec", - (type, _args) => new EnemyDamageReducerModifier(type, 2.5), - ), + ENEMY_DAMAGE_BOOSTER: () => new TrainerItemReward(TrainerItemId.ENEMY_DAMAGE_BOOSTER), + ENEMY_DAMAGE_REDUCTION: () => new TrainerItemReward(TrainerItemId.ENEMY_DAMAGE_REDUCTION), //ENEMY_SUPER_EFFECT_BOOSTER: () => new ModifierType('Type Advantage Token', 'Increases damage of super effective attacks by 30%', (type, _args) => new EnemySuperEffectiveDamageBoosterModifier(type, 30), 'wl_custom_super_effective'), - ENEMY_HEAL: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_HEAL", - "wl_potion", - (type, _args) => new EnemyTurnHealModifier(type, 2, 10), - ), - ENEMY_ATTACK_POISON_CHANCE: () => - new EnemyAttackStatusEffectChanceModifierType( - "modifierType:ModifierType.ENEMY_ATTACK_POISON_CHANCE", - "wl_antidote", - 5, - StatusEffect.POISON, - 10, - ), - ENEMY_ATTACK_PARALYZE_CHANCE: () => - new EnemyAttackStatusEffectChanceModifierType( - "modifierType:ModifierType.ENEMY_ATTACK_PARALYZE_CHANCE", - "wl_paralyze_heal", - 2.5, - StatusEffect.PARALYSIS, - 10, - ), - ENEMY_ATTACK_BURN_CHANCE: () => - new EnemyAttackStatusEffectChanceModifierType( - "modifierType:ModifierType.ENEMY_ATTACK_BURN_CHANCE", - "wl_burn_heal", - 5, - StatusEffect.BURN, - 10, - ), - ENEMY_STATUS_EFFECT_HEAL_CHANCE: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_STATUS_EFFECT_HEAL_CHANCE", - "wl_full_heal", - (type, _args) => new EnemyStatusEffectHealChanceModifier(type, 2.5, 10), - ), - ENEMY_ENDURE_CHANCE: () => - new EnemyEndureChanceModifierType("modifierType:ModifierType.ENEMY_ENDURE_CHANCE", "wl_reset_urge", 2), - ENEMY_FUSED_CHANCE: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_FUSED_CHANCE", - "wl_custom_spliced", - (type, _args) => new EnemyFusionChanceModifier(type, 1), - ), + ENEMY_HEAL: () => new TrainerItemReward(TrainerItemId.ENEMY_HEAL), + ENEMY_ATTACK_POISON_CHANCE: () => new TrainerItemReward(TrainerItemId.ENEMY_ATTACK_POISON_CHANCE), + ENEMY_ATTACK_PARALYZE_CHANCE: () => new TrainerItemReward(TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE), + ENEMY_ATTACK_BURN_CHANCE: () => new TrainerItemReward(TrainerItemId.ENEMY_ATTACK_BURN_CHANCE), + ENEMY_STATUS_EFFECT_HEAL_CHANCE: () => new TrainerItemReward(TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE), + ENEMY_ENDURE_CHANCE: () => new TrainerItemReward(TrainerItemId.ENEMY_ENDURE_CHANCE), + ENEMY_FUSED_CHANCE: () => new TrainerItemReward(TrainerItemId.ENEMY_FUSED_CHANCE), - MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () => - new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs) { - return new PokemonBaseStatTotalModifierType(pregenArgs[0] as 10 | -15); - } - return new PokemonBaseStatTotalModifierType(10); - }), - MYSTERY_ENCOUNTER_OLD_GATEAU: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU", - "old_gateau", - (type, args) => new PokemonBaseStatFlatModifier(type, (args[0] as Pokemon).id), - ), - MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => - new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs) { - return new ModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", - "black_sludge", - (type, _args) => new HealShopCostModifier(type, pregenArgs[0] as number), - ); - } - return new ModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", - "black_sludge", - (type, _args) => new HealShopCostModifier(type, 2.5), - ); - }), - MYSTERY_ENCOUNTER_MACHO_BRACE: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_MACHO_BRACE", - "macho_brace", - (type, args) => new PokemonIncrementingStatModifier(type, (args[0] as Pokemon).id), - ), - MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => - new ModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET", - "golden_net", - (type, _args) => new BoostBugSpawnModifier(type), - ), + MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD: () => new HeldItemReward(HeldItemId.SHUCKLE_JUICE_GOOD), + MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD: () => new HeldItemReward(HeldItemId.SHUCKLE_JUICE_BAD), + + MYSTERY_ENCOUNTER_OLD_GATEAU: () => new HeldItemReward(HeldItemId.OLD_GATEAU), + + MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new TrainerItemReward(TrainerItemId.BLACK_SLUDGE), + + MYSTERY_ENCOUNTER_MACHO_BRACE: () => new HeldItemReward(HeldItemId.MACHO_BRACE), + + MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => new TrainerItemReward(TrainerItemId.GOLDEN_BUG_NET), }); /** @@ -2342,19 +1579,6 @@ export interface ModifierPool { let modifierPoolThresholds = {}; let ignoredPoolIndexes = {}; -let dailyStarterModifierPoolThresholds = {}; -// biome-ignore lint/correctness/noUnusedVariables: TODO explain why this is marked as OK -let ignoredDailyStarterPoolIndexes = {}; - -let enemyModifierPoolThresholds = {}; -// biome-ignore lint/correctness/noUnusedVariables: TODO explain why this is marked as OK -let enemyIgnoredPoolIndexes = {}; - -let enemyBuffModifierPoolThresholds = {}; -// biome-ignore lint/correctness/noUnusedVariables: TODO explain why this is marked as OK -let enemyBuffIgnoredPoolIndexes = {}; - -const tierWeights = [768 / 1024, 195 / 1024, 48 / 1024, 12 / 1024, 1 / 1024]; /** * Allows a unit test to check if an item exists in the Modifier Pool. Checks the pool directly, rather than attempting to reroll for the item. */ @@ -2367,30 +1591,27 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod }); const ignoredIndexes = {}; - const modifierTableData = {}; const thresholds = Object.fromEntries( new Map( Object.keys(pool).map(t => { ignoredIndexes[t] = []; const thresholds = new Map(); const tierModifierIds: string[] = []; - let tierMaxWeight = 0; let i = 0; pool[t].reduce((total: number, modifierType: WeightedModifierType) => { const weightedModifierType = modifierType as WeightedModifierType; - const existingModifiers = globalScene.findModifiers( - m => m.type.id === weightedModifierType.modifierType.id, - poolType === ModifierPoolType.PLAYER, - ); const itemModifierType = weightedModifierType.modifierType instanceof ModifierTypeGenerator ? weightedModifierType.modifierType.generateType(party) : weightedModifierType.modifierType; + const trainerItemfullStack = + itemModifierType instanceof TrainerItemReward + ? globalScene.trainerItems.isMaxStack(itemModifierType.itemId) + : false; const weight = - !existingModifiers.length || - itemModifierType instanceof PokemonHeldItemModifierType || - itemModifierType instanceof FormChangeItemModifierType || - existingModifiers.find(m => m.stackCount < m.getMaxStackCount(true)) + !trainerItemfullStack || + itemModifierType instanceof HeldItemReward || + itemModifierType instanceof FormChangeItemReward ? weightedModifierType.weight instanceof Function ? // biome-ignore lint/complexity/noBannedTypes: TODO: refactor to not use Function type (weightedModifierType.weight as Function)(party, rerollCount) @@ -2399,14 +1620,6 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod if (weightedModifierType.maxWeight) { const modifierId = weightedModifierType.modifierType.id; tierModifierIds.push(modifierId); - const outputWeight = useMaxWeightForOutput ? weightedModifierType.maxWeight : weight; - modifierTableData[modifierId] = { - weight: outputWeight, - tier: Number.parseInt(t), - tierPercent: 0, - totalPercent: 0, - }; - tierMaxWeight += outputWeight; } if (weight) { total += weight; @@ -2420,46 +1633,20 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod thresholds.set(total, i++); return total; }, 0); - for (const id of tierModifierIds) { - modifierTableData[id].tierPercent = Math.floor((modifierTableData[id].weight / tierMaxWeight) * 10000) / 100; - } return [t, Object.fromEntries(thresholds)]; }), ), ); - for (const id of Object.keys(modifierTableData)) { - modifierTableData[id].totalPercent = - Math.floor(modifierTableData[id].tierPercent * tierWeights[modifierTableData[id].tier] * 100) / 100; - modifierTableData[id].tier = ModifierTier[modifierTableData[id].tier]; - } - if (outputModifierData) { - console.table(modifierTableData); - } switch (poolType) { case ModifierPoolType.PLAYER: modifierPoolThresholds = thresholds; ignoredPoolIndexes = ignoredIndexes; break; - case ModifierPoolType.WILD: - case ModifierPoolType.TRAINER: - enemyModifierPoolThresholds = thresholds; - enemyIgnoredPoolIndexes = ignoredIndexes; - break; - case ModifierPoolType.ENEMY_BUFF: - enemyBuffModifierPoolThresholds = thresholds; - enemyBuffIgnoredPoolIndexes = ignoredIndexes; - break; - case ModifierPoolType.DAILY_STARTER: - dailyStarterModifierPoolThresholds = thresholds; - ignoredDailyStarterPoolIndexes = ignoredIndexes; - break; } } export interface CustomModifierSettings { - /** If specified, will override the next X items to be the specified tier. These can upgrade with luck. */ - guaranteedModifierTiers?: ModifierTier[]; - /** If specified, will override the first X items to be specific modifier options (these should be pre-genned). */ + guaranteedModifierTiers?: RewardTier[]; guaranteedModifierTypeOptions?: ModifierTypeOption[]; /** If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). */ guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; @@ -2499,7 +1686,7 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc { export function getPlayerModifierTypeOptions( count: number, party: PlayerPokemon[], - modifierTiers?: ModifierTier[], + modifierTiers?: RewardTier[], customModifierSettings?: CustomModifierSettings, ): ModifierTypeOption[] { const options: ModifierTypeOption[] = []; @@ -2574,7 +1761,7 @@ function getModifierTypeOptionWithRetry( existingOptions: ModifierTypeOption[], retryCount: number, party: PlayerPokemon[], - tier?: ModifierTier, + tier?: RewardTier, allowLuckUpgrades?: boolean, ): ModifierTypeOption { allowLuckUpgrades = allowLuckUpgrades ?? true; @@ -2585,6 +1772,11 @@ function getModifierTypeOptionWithRetry( ++r < retryCount && existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length ) { + console.log("Retry count:", r); + console.log(candidate?.type.group); + console.log(candidate?.type.name); + console.log(existingOptions.filter(o => o.type.name === candidate?.type.name).length); + console.log(existingOptions.filter(o => o.type.group === candidate?.type.group).length); candidate = getNewModifierTypeOption( party, ModifierPoolType.PLAYER, @@ -2656,90 +1848,6 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseC return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat(); } -export function getEnemyBuffModifierForWave( - tier: ModifierTier, - enemyModifiers: PersistentModifier[], -): EnemyPersistentModifier { - let tierStackCount: number; - switch (tier) { - case ModifierTier.ULTRA: - tierStackCount = 5; - break; - case ModifierTier.GREAT: - tierStackCount = 3; - break; - default: - tierStackCount = 1; - break; - } - - const retryCount = 50; - let candidate = getNewModifierTypeOption([], ModifierPoolType.ENEMY_BUFF, tier); - let r = 0; - let matchingModifier: PersistentModifier | undefined; - while ( - ++r < retryCount && - (matchingModifier = enemyModifiers.find(m => m.type.id === candidate?.type?.id)) && - matchingModifier.getMaxStackCount() < matchingModifier.stackCount + (r < 10 ? tierStackCount : 1) - ) { - candidate = getNewModifierTypeOption([], ModifierPoolType.ENEMY_BUFF, tier); - } - - const modifier = candidate?.type?.newModifier() as EnemyPersistentModifier; - modifier.stackCount = tierStackCount; - - return modifier; -} - -export function getEnemyModifierTypesForWave( - waveIndex: number, - count: number, - party: EnemyPokemon[], - poolType: ModifierPoolType.WILD | ModifierPoolType.TRAINER, - upgradeChance = 0, -): PokemonHeldItemModifierType[] { - const ret = new Array(count) - .fill(0) - .map( - () => - getNewModifierTypeOption(party, poolType, undefined, upgradeChance && !randSeedInt(upgradeChance) ? 1 : 0) - ?.type as PokemonHeldItemModifierType, - ); - if (!(waveIndex % 1000)) { - ret.push(getModifierType(modifierTypeInitObj.MINI_BLACK_HOLE) as PokemonHeldItemModifierType); - } - return ret; -} - -export function getDailyRunStarterModifiers(party: PlayerPokemon[]): PokemonHeldItemModifier[] { - const ret: PokemonHeldItemModifier[] = []; - for (const p of party) { - for (let m = 0; m < 3; m++) { - const tierValue = randSeedInt(64); - - let tier: ModifierTier; - if (tierValue > 25) { - tier = ModifierTier.COMMON; - } else if (tierValue > 12) { - tier = ModifierTier.GREAT; - } else if (tierValue > 4) { - tier = ModifierTier.ULTRA; - } else if (tierValue) { - tier = ModifierTier.ROGUE; - } else { - tier = ModifierTier.MASTER; - } - - const modifier = getNewModifierTypeOption(party, ModifierPoolType.DAILY_STARTER, tier)?.type?.newModifier( - p, - ) as PokemonHeldItemModifier; - ret.push(modifier); - } - } - - return ret; -} - /** * Generates a ModifierType from the specified pool * @param party party of the trainer using the item @@ -2752,84 +1860,24 @@ export function getDailyRunStarterModifiers(party: PlayerPokemon[]): PokemonHeld function getNewModifierTypeOption( party: Pokemon[], poolType: ModifierPoolType, - tier?: ModifierTier, + baseTier?: RewardTier, upgradeCount?: number, retryCount = 0, allowLuckUpgrades = true, ): ModifierTypeOption | null { const player = !poolType; const pool = getModifierPoolForType(poolType); - let thresholds: object; - switch (poolType) { - case ModifierPoolType.PLAYER: - thresholds = modifierPoolThresholds; - break; - case ModifierPoolType.WILD: - thresholds = enemyModifierPoolThresholds; - break; - case ModifierPoolType.TRAINER: - thresholds = enemyModifierPoolThresholds; - break; - case ModifierPoolType.ENEMY_BUFF: - thresholds = enemyBuffModifierPoolThresholds; - break; - case ModifierPoolType.DAILY_STARTER: - thresholds = dailyStarterModifierPoolThresholds; - break; + const thresholds = getPoolThresholds(poolType); + + let tier = 0; + if (isNullOrUndefined(baseTier)) { + baseTier = randomBaseTier(); } - if (tier === undefined) { - const tierValue = randSeedInt(1024); - if (!upgradeCount) { - upgradeCount = 0; - } - if (player && tierValue && allowLuckUpgrades) { - const partyLuckValue = getPartyLuckValue(party); - const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4)); - let upgraded = false; - do { - upgraded = randSeedInt(upgradeOdds) < 4; - if (upgraded) { - upgradeCount++; - } - } while (upgraded); - } - - if (tierValue > 255) { - tier = ModifierTier.COMMON; - } else if (tierValue > 60) { - tier = ModifierTier.GREAT; - } else if (tierValue > 12) { - tier = ModifierTier.ULTRA; - } else if (tierValue) { - tier = ModifierTier.ROGUE; - } else { - tier = ModifierTier.MASTER; - } - - tier += upgradeCount; - while (tier && (!pool.hasOwnProperty(tier) || !pool[tier].length)) { - tier--; - if (upgradeCount) { - upgradeCount--; - } - } - } else if (upgradeCount === undefined && player) { - upgradeCount = 0; - if (tier < ModifierTier.MASTER && allowLuckUpgrades) { - const partyLuckValue = getPartyLuckValue(party); - const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4)); - while (pool.hasOwnProperty(tier + upgradeCount + 1) && pool[tier + upgradeCount + 1].length) { - if (randSeedInt(upgradeOdds) < 4) { - upgradeCount++; - } else { - break; - } - } - tier += upgradeCount; - } - } else if (retryCount >= 100 && tier) { - retryCount = 0; - tier--; + if (isNullOrUndefined(upgradeCount)) { + upgradeCount = allowLuckUpgrades ? getUpgradeCount(party, player, baseTier) : 0; + tier = baseTier + upgradeCount; + } else { + tier = baseTier; } const tierThresholds = Object.keys(thresholds[tier]); @@ -2856,7 +1904,7 @@ function getNewModifierTypeOption( modifierType = (modifierType as ModifierTypeGenerator).generateType(party); if (modifierType === null) { if (player) { - console.log(ModifierTier[tier], upgradeCount); + console.log(RewardTier[tier], upgradeCount); } return getNewModifierTypeOption(party, poolType, tier, upgradeCount, ++retryCount); } @@ -2867,9 +1915,61 @@ function getNewModifierTypeOption( return new ModifierTypeOption(modifierType as ModifierType, upgradeCount!); // TODO: is this bang correct? } -export function getDefaultModifierTypeForTier(tier: ModifierTier): ModifierType { +function getPoolThresholds(poolType: ModifierPoolType) { + let thresholds: object; + switch (poolType) { + case ModifierPoolType.PLAYER: + thresholds = modifierPoolThresholds; + break; + } + return thresholds; +} + +function randomBaseTier(): RewardTier { + const tierValue = randSeedInt(1024); + + if (tierValue > 255) { + return RewardTier.COMMON; + } + if (tierValue > 60) { + return RewardTier.GREAT; + } + if (tierValue > 12) { + return RewardTier.ULTRA; + } + if (tierValue) { + return RewardTier.ROGUE; + } + return RewardTier.MASTER; +} + +function getUpgradeCount( + party: Pokemon[], + player: boolean, + baseTier: RewardTier, + allowLuckUpgrades = true, +): RewardTier { + const pool = getModifierPoolForType(ModifierPoolType.PLAYER); + let upgradeCount = 0; + if (player) { + if (baseTier < RewardTier.MASTER && allowLuckUpgrades) { + const partyLuckValue = getPartyLuckValue(party); + const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4)); + while (pool.hasOwnProperty(baseTier + upgradeCount + 1) && pool[baseTier + upgradeCount + 1].length) { + if (randSeedInt(upgradeOdds) < 4) { + upgradeCount++; + } else { + break; + } + } + } + } + return upgradeCount; +} + +export function getDefaultModifierTypeForTier(tier: RewardTier): ModifierType { const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER); - let modifierType: ModifierType | WeightedModifierType = modifierPool[tier || ModifierTier.COMMON][0]; + let modifierType: ModifierType | WeightedModifierType = modifierPool[tier || RewardTier.COMMON][0]; if (modifierType instanceof WeightedModifierType) { modifierType = (modifierType as WeightedModifierType).modifierType; } @@ -2921,19 +2021,19 @@ export function getLuckString(luckValue: number): string { } export function getLuckTextTint(luckValue: number): number { - let modifierTier: ModifierTier; + let modifierTier: RewardTier; if (luckValue > 11) { - modifierTier = ModifierTier.LUXURY; + modifierTier = RewardTier.LUXURY; } else if (luckValue > 9) { - modifierTier = ModifierTier.MASTER; + modifierTier = RewardTier.MASTER; } else if (luckValue > 5) { - modifierTier = ModifierTier.ROGUE; + modifierTier = RewardTier.ROGUE; } else if (luckValue > 2) { - modifierTier = ModifierTier.ULTRA; + modifierTier = RewardTier.ULTRA; } else if (luckValue) { - modifierTier = ModifierTier.GREAT; + modifierTier = RewardTier.GREAT; } else { - modifierTier = ModifierTier.COMMON; + modifierTier = RewardTier.COMMON; } return getModifierTierTextTint(modifierTier); } @@ -2948,7 +2048,6 @@ export function initModifierTypes() { // For now, doing the minimal work until the modifier rework lands. const ModifierTypeConstructorMap = Object.freeze({ ModifierTypeGenerator, - PokemonHeldItemModifierType, }); /** diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 93fb5f5b6f4..716937289b4 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1,156 +1,33 @@ import { FusionSpeciesFormEvolution, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; -import { getBerryEffectFunc, getBerryPredicate } from "#app/data/berry"; import { getLevelTotalExp } from "#app/data/exp"; -import { allMoves, modifierTypes } from "#app/data/data-lists"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; -import { SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms/form-change-triggers"; -import type { FormChangeItem } from "#enums/form-change-item"; -import { getStatusEffectHealText } from "#app/data/status-effect"; -import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { getPokemonNameWithAffix } from "#app/messages"; +import type { PlayerPokemon } from "#app/field/pokemon"; import Overrides from "#app/overrides"; import { LearnMoveType } from "#enums/learn-move-type"; import type { VoucherType } from "#app/system/voucher"; -import { Command } from "#enums/command"; -import { addTextObject, TextStyle } from "#app/ui/text"; -import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#app/utils/common"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; -import type { MoveId } from "#enums/move-id"; +import { isNullOrUndefined, NumberHolder } from "#app/utils/common"; import type { Nature } from "#enums/nature"; import type { PokeballType } from "#enums/pokeball"; import { SpeciesId } from "#enums/species-id"; -import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; -import { StatusEffect } from "#enums/status-effect"; import type { PokemonType } from "#enums/pokemon-type"; -import i18next from "i18next"; import type { - DoubleBattleChanceBoosterModifierType, EvolutionItemModifierType, - FormChangeItemModifierType, - ModifierOverride, ModifierType, - PokemonBaseStatTotalModifierType, - PokemonExpBoosterModifierType, - PokemonFriendshipBoosterModifierType, - PokemonMoveAccuracyBoosterModifierType, - PokemonMultiHitModifierType, TerastallizeModifierType, TmModifierType, } from "./modifier-type"; -import { getModifierType } from "#app/utils/modifier-utils"; -import { Color, ShadowColor } from "#enums/color"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; -import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import type { ModifierInstanceMap, ModifierString } from "#app/@types/modifier-types"; +import { assignItemsFromConfiguration } from "#app/items/held-item-pool"; +import type { HeldItemConfiguration } from "#app/items/held-item-data-types"; +import { HeldItemId } from "#enums/held-item-id"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; +import type { TrainerItemConfiguration } from "#app/items/trainer-item-data-types"; export type ModifierPredicate = (modifier: Modifier) => boolean; -const iconOverflowIndex = 24; - -export const modifierSortFunc = (a: Modifier, b: Modifier): number => { - const itemNameMatch = a.type.name.localeCompare(b.type.name); - const typeNameMatch = a.constructor.name.localeCompare(b.constructor.name); - const aId = a instanceof PokemonHeldItemModifier && a.pokemonId ? a.pokemonId : 4294967295; - const bId = b instanceof PokemonHeldItemModifier && b.pokemonId ? b.pokemonId : 4294967295; - - //First sort by pokemonID - if (aId < bId) { - return 1; - } - if (aId > bId) { - return -1; - } - if (aId === bId) { - //Then sort by item type - if (typeNameMatch === 0) { - return itemNameMatch; - //Finally sort by item name - } - return typeNameMatch; - } - return 0; -}; - -export class ModifierBar extends Phaser.GameObjects.Container { - private player: boolean; - private modifierCache: PersistentModifier[]; - - constructor(enemy?: boolean) { - super(globalScene, 1 + (enemy ? 302 : 0), 2); - - this.player = !enemy; - this.setScale(0.5); - } - - /** - * Method to update content displayed in {@linkcode ModifierBar} - * @param {PersistentModifier[]} modifiers - The list of modifiers to be displayed in the {@linkcode ModifierBar} - * @param {boolean} hideHeldItems - If set to "true", only modifiers not assigned to a Pokémon are displayed - */ - updateModifiers(modifiers: PersistentModifier[], hideHeldItems = false) { - this.removeAll(true); - - const visibleIconModifiers = modifiers.filter(m => m.isIconVisible()); - const nonPokemonSpecificModifiers = visibleIconModifiers - .filter(m => !(m as PokemonHeldItemModifier).pokemonId) - .sort(modifierSortFunc); - const pokemonSpecificModifiers = visibleIconModifiers - .filter(m => (m as PokemonHeldItemModifier).pokemonId) - .sort(modifierSortFunc); - - const sortedVisibleIconModifiers = hideHeldItems - ? nonPokemonSpecificModifiers - : nonPokemonSpecificModifiers.concat(pokemonSpecificModifiers); - - sortedVisibleIconModifiers.forEach((modifier: PersistentModifier, i: number) => { - const icon = modifier.getIcon(); - if (i >= iconOverflowIndex) { - icon.setVisible(false); - } - this.add(icon); - this.setModifierIconPosition(icon, sortedVisibleIconModifiers.length); - icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 24), Phaser.Geom.Rectangle.Contains); - icon.on("pointerover", () => { - globalScene.ui.showTooltip(modifier.type.name, modifier.type.getDescription()); - if (this.modifierCache && this.modifierCache.length > iconOverflowIndex) { - this.updateModifierOverflowVisibility(true); - } - }); - icon.on("pointerout", () => { - globalScene.ui.hideTooltip(); - if (this.modifierCache && this.modifierCache.length > iconOverflowIndex) { - this.updateModifierOverflowVisibility(false); - } - }); - }); - - for (const icon of this.getAll()) { - this.sendToBack(icon); - } - - this.modifierCache = modifiers; - } - - updateModifierOverflowVisibility(ignoreLimit: boolean) { - const modifierIcons = this.getAll().reverse(); - for (const modifier of modifierIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) { - modifier.setVisible(ignoreLimit); - } - } - - setModifierIconPosition(icon: Phaser.GameObjects.Container, modifierCount: number) { - const rowIcons: number = 12 + 6 * Math.max(Math.ceil(Math.min(modifierCount, 24) / 12) - 2, 0); - - const x = ((this.getIndex(icon) % rowIcons) * 26) / (rowIcons / 12); - const y = Math.floor(this.getIndex(icon) / rowIcons) * 20; - - icon.setPosition(this.player ? x : -x, y); - } -} - export abstract class Modifier { public type: ModifierType; @@ -195,106 +72,6 @@ export abstract class Modifier { abstract apply(...args: unknown[]): boolean; } -export abstract class PersistentModifier extends Modifier { - public stackCount: number; - public virtualStackCount: number; - - /** This field does not exist at runtime and must not be used. - * Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called. - */ - private declare _: never; - - constructor(type: ModifierType, stackCount = 1) { - super(type); - this.stackCount = stackCount; - this.virtualStackCount = 0; - } - - add(modifiers: PersistentModifier[], virtual: boolean): boolean { - for (const modifier of modifiers) { - if (this.match(modifier)) { - return modifier.incrementStack(this.stackCount, virtual); - } - } - - if (virtual) { - this.virtualStackCount += this.stackCount; - this.stackCount = 0; - } - modifiers.push(this); - return true; - } - - abstract clone(): PersistentModifier; - - getArgs(): any[] { - return []; - } - - incrementStack(amount: number, virtual: boolean): boolean { - if (this.getStackCount() + amount <= this.getMaxStackCount()) { - if (!virtual) { - this.stackCount += amount; - } else { - this.virtualStackCount += amount; - } - return true; - } - - return false; - } - - getStackCount(): number { - return this.stackCount + this.virtualStackCount; - } - - abstract getMaxStackCount(forThreshold?: boolean): number; - - getCountUnderMax(): number { - return this.getMaxStackCount() - this.getStackCount(); - } - - isIconVisible(): boolean { - return true; - } - - getIcon(_forSummary?: boolean): Phaser.GameObjects.Container { - const container = globalScene.add.container(0, 0); - - const item = globalScene.add.sprite(0, 12, "items"); - item.setFrame(this.type.iconImage); - item.setOrigin(0, 0.5); - container.add(item); - - const stackText = this.getIconStackText(); - if (stackText) { - container.add(stackText); - } - - const virtualStackText = this.getIconStackText(true); - if (virtualStackText) { - container.add(virtualStackText); - } - - return container; - } - - getIconStackText(virtual?: boolean): Phaser.GameObjects.BitmapText | null { - if (this.getMaxStackCount() === 1 || (virtual && !this.virtualStackCount)) { - return null; - } - - const text = globalScene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); - text.letterSpacing = -0.5; - if (this.getStackCount() >= this.getMaxStackCount()) { - text.setTint(0xf89890); - } - text.setOrigin(0, 0); - - return text; - } -} - export abstract class ConsumableModifier extends Modifier { add(_modifiers: Modifier[]): boolean { return true; @@ -352,1710 +129,6 @@ export class AddVoucherModifier extends ConsumableModifier { } } -/** - * Modifier used for party-wide or passive items that start an initial - * {@linkcode battleCount} equal to {@linkcode maxBattles} that, for every - * battle, decrements. Typically, when {@linkcode battleCount} reaches 0, the - * modifier will be removed. If a modifier of the same type is to be added, it - * will reset {@linkcode battleCount} back to {@linkcode maxBattles} of the - * existing modifier instead of adding that modifier directly. - * @extends PersistentModifier - * @abstract - * @see {@linkcode add} - */ -export abstract class LapsingPersistentModifier extends PersistentModifier { - /** The maximum amount of battles the modifier will exist for */ - private maxBattles: number; - /** The current amount of battles the modifier will exist for */ - private battleCount: number; - - constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) { - super(type, stackCount); - - this.maxBattles = maxBattles; - this.battleCount = battleCount ?? this.maxBattles; - } - - /** - * Goes through existing modifiers for any that match the selected modifier, - * which will then either add it to the existing modifiers if none were found - * or, if one was found, it will refresh {@linkcode battleCount}. - * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers - * @param _virtual N/A - * @param _scene N/A - * @returns `true` if the modifier was successfully added or applied, false otherwise - */ - add(modifiers: PersistentModifier[], _virtual: boolean): boolean { - for (const modifier of modifiers) { - if (this.match(modifier)) { - const modifierInstance = modifier as LapsingPersistentModifier; - if (modifierInstance.getBattleCount() < modifierInstance.getMaxBattles()) { - modifierInstance.resetBattleCount(); - globalScene.playSound("se/restore"); - return true; - } - // should never get here - return false; - } - } - - modifiers.push(this); - return true; - } - - /** - * Lapses the {@linkcode battleCount} by 1. - * @param _args passed arguments (not in use here) - * @returns `true` if the {@linkcode battleCount} is greater than 0 - */ - public lapse(..._args: unknown[]): boolean { - this.battleCount--; - return this.battleCount > 0; - } - - getIcon(): Phaser.GameObjects.Container { - const container = super.getIcon(); - - // Linear interpolation on hue - const hue = Math.floor(120 * (this.battleCount / this.maxBattles) + 5); - - // Generates the color hex code with a constant saturation and lightness but varying hue - const typeHex = hslToHex(hue, 0.5, 0.9); - const strokeHex = hslToHex(hue, 0.7, 0.3); - - const battleCountText = addTextObject(27, 0, this.battleCount.toString(), TextStyle.PARTY, { - fontSize: "66px", - color: typeHex, - }); - battleCountText.setShadow(0, 0); - battleCountText.setStroke(strokeHex, 16); - battleCountText.setOrigin(1, 0); - container.add(battleCountText); - - return container; - } - - getIconStackText(_virtual?: boolean): Phaser.GameObjects.BitmapText | null { - return null; - } - - getBattleCount(): number { - return this.battleCount; - } - - resetBattleCount(): void { - this.battleCount = this.maxBattles; - } - - /** - * Updates an existing modifier with a new `maxBattles` and `battleCount`. - */ - setNewBattleCount(count: number): void { - this.maxBattles = count; - this.battleCount = count; - } - - getMaxBattles(): number { - return this.maxBattles; - } - - getArgs(): any[] { - return [this.maxBattles, this.battleCount]; - } - - getMaxStackCount(_forThreshold?: boolean): number { - // Must be an abitrary number greater than 1 - return 2; - } -} - -/** - * Modifier used for passive items, specifically lures, that - * temporarily increases the chance of a double battle. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} - */ -export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier { - public override type: DoubleBattleChanceBoosterModifierType; - - match(modifier: Modifier): boolean { - return modifier instanceof DoubleBattleChanceBoosterModifier && modifier.getMaxBattles() === this.getMaxBattles(); - } - - clone(): DoubleBattleChanceBoosterModifier { - return new DoubleBattleChanceBoosterModifier( - this.type, - this.getMaxBattles(), - this.getBattleCount(), - this.stackCount, - ); - } - - /** - * Increases the chance of a double battle occurring - * @param doubleBattleChance {@linkcode NumberHolder} for double battle chance - * @returns true - */ - override apply(doubleBattleChance: NumberHolder): boolean { - // This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using randSeedInt - // A double battle will initiate if the generated number is 0 - doubleBattleChance.value = doubleBattleChance.value / 4; - - return true; - } -} - -/** - * Modifier used for party-wide items, specifically the X items, that - * temporarily increases the stat stage multiplier of the corresponding - * {@linkcode TempBattleStat}. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} - */ -export class TempStatStageBoosterModifier extends LapsingPersistentModifier { - /** The stat whose stat stage multiplier will be temporarily increased */ - private stat: TempBattleStat; - /** The amount by which the stat stage itself or its multiplier will be increased by */ - private boost: number; - - constructor(type: ModifierType, stat: TempBattleStat, maxBattles: number, battleCount?: number, stackCount?: number) { - super(type, maxBattles, battleCount, stackCount); - - this.stat = stat; - // Note that, because we want X Accuracy to maintain its original behavior, - // it will increment as it did previously, directly to the stat stage. - this.boost = stat !== Stat.ACC ? 0.3 : 1; - } - - match(modifier: Modifier): boolean { - if (modifier instanceof TempStatStageBoosterModifier) { - const modifierInstance = modifier as TempStatStageBoosterModifier; - return modifierInstance.stat === this.stat; - } - return false; - } - - clone() { - return new TempStatStageBoosterModifier( - this.type, - this.stat, - this.getMaxBattles(), - this.getBattleCount(), - this.stackCount, - ); - } - - getArgs(): any[] { - return [this.stat, ...super.getArgs()]; - } - - /** - * Checks if {@linkcode args} contains the necessary elements and if the - * incoming stat is matches {@linkcode stat}. - * @param tempBattleStat {@linkcode TempBattleStat} being affected - * @param statLevel {@linkcode NumberHolder} that holds the resulting value of the stat stage multiplier - * @returns `true` if the modifier can be applied, false otherwise - */ - override shouldApply(tempBattleStat?: TempBattleStat, statLevel?: NumberHolder): boolean { - return ( - !!tempBattleStat && !!statLevel && TEMP_BATTLE_STATS.includes(tempBattleStat) && tempBattleStat === this.stat - ); - } - - /** - * Increases the incoming stat stage matching {@linkcode stat} by {@linkcode boost}. - * @param _tempBattleStat {@linkcode TempBattleStat} N/A - * @param statLevel {@linkcode NumberHolder} that holds the resulting value of the stat stage multiplier - */ - override apply(_tempBattleStat: TempBattleStat, statLevel: NumberHolder): boolean { - statLevel.value += this.boost; - return true; - } -} - -/** - * Modifier used for party-wide items, namely Dire Hit, that - * temporarily increments the critical-hit stage - * @extends LapsingPersistentModifier - * @see {@linkcode apply} - */ -export class TempCritBoosterModifier extends LapsingPersistentModifier { - clone() { - return new TempCritBoosterModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount); - } - - match(modifier: Modifier): boolean { - return modifier instanceof TempCritBoosterModifier; - } - - /** - * Checks if {@linkcode args} contains the necessary elements. - * @param critLevel {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns `true` if the critical-hit stage boost applies successfully - */ - override shouldApply(critLevel?: NumberHolder): boolean { - return !!critLevel; - } - - /** - * Increases the current critical-hit stage value by 1. - * @param critLevel {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns `true` if the critical-hit stage boost applies successfully - */ - override apply(critLevel: NumberHolder): boolean { - critLevel.value++; - return true; - } -} - -export class MapModifier extends PersistentModifier { - clone(): MapModifier { - return new MapModifier(this.type, this.stackCount); - } - - override apply(..._args: unknown[]): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export class MegaEvolutionAccessModifier extends PersistentModifier { - clone(): MegaEvolutionAccessModifier { - return new MegaEvolutionAccessModifier(this.type, this.stackCount); - } - - override apply(..._args: unknown[]): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export class GigantamaxAccessModifier extends PersistentModifier { - clone(): GigantamaxAccessModifier { - return new GigantamaxAccessModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode GigantamaxAccessModifier} - * @param _args N/A - * @returns always `true` - */ - apply(..._args: unknown[]): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export class TerastallizeAccessModifier extends PersistentModifier { - clone(): TerastallizeAccessModifier { - return new TerastallizeAccessModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode TerastallizeAccessModifier} - * @param _args N/A - * @returns always `true` - */ - override apply(..._args: unknown[]): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export abstract class PokemonHeldItemModifier extends PersistentModifier { - /** The ID of the {@linkcode Pokemon} that this item belongs to. */ - public pokemonId: number; - /** Whether this item can be transfered to or stolen by another Pokemon. */ - public isTransferable = true; - - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, stackCount); - - this.pokemonId = pokemonId; - } - - abstract matchType(_modifier: Modifier): boolean; - - match(modifier: Modifier) { - return this.matchType(modifier) && (modifier as PokemonHeldItemModifier).pokemonId === this.pokemonId; - } - - getArgs(): any[] { - return [this.pokemonId]; - } - - /** - * Applies the {@linkcode PokemonHeldItemModifier} to the given {@linkcode Pokemon}. - * @param pokemon The {@linkcode Pokemon} that holds the held item - * @param args additional parameters - */ - abstract override apply(pokemon: Pokemon, ...args: unknown[]): boolean; - - /** - * Checks if {@linkcode PokemonHeldItemModifier} should be applied. - * @param pokemon The {@linkcode Pokemon} that holds the item - * @param _args N/A - * @returns if {@linkcode PokemonHeldItemModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, ..._args: unknown[]): boolean { - return !!pokemon && (this.pokemonId === -1 || pokemon.id === this.pokemonId); - } - - isIconVisible(): boolean { - return !!this.getPokemon()?.isOnField(); - } - - getIcon(forSummary?: boolean): Phaser.GameObjects.Container { - const container = !forSummary ? globalScene.add.container(0, 0) : super.getIcon(); - - if (!forSummary) { - const pokemon = this.getPokemon(); - if (pokemon) { - const pokemonIcon = globalScene.addPokemonIcon(pokemon, -2, 10, 0, 0.5, undefined, true); - container.add(pokemonIcon); - container.setName(pokemon.id.toString()); - } - - const item = globalScene.add.sprite(16, this.virtualStackCount ? 8 : 16, "items"); - item.setScale(0.5); - item.setOrigin(0, 0.5); - item.setTexture("items", this.type.iconImage); - container.add(item); - - const stackText = this.getIconStackText(); - if (stackText) { - container.add(stackText); - } - - const virtualStackText = this.getIconStackText(true); - if (virtualStackText) { - container.add(virtualStackText); - } - } else { - container.setScale(0.5); - } - - return container; - } - - getPokemon(): Pokemon | undefined { - return globalScene.getPokemonById(this.pokemonId) ?? undefined; - } - - getScoreMultiplier(): number { - return 1; - } - - getMaxStackCount(forThreshold?: boolean): number { - const pokemon = this.getPokemon(); - if (!pokemon) { - return 0; - } - if (pokemon.isPlayer() && forThreshold) { - return globalScene - .getPlayerParty() - .map(p => this.getMaxHeldItemCount(p)) - .reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0); - } - return this.getMaxHeldItemCount(pokemon); - } - - getSpecies(): SpeciesId | null { - return null; - } - - abstract getMaxHeldItemCount(pokemon?: Pokemon): number; -} - -export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModifier { - protected battlesLeft: number; - public isTransferable = false; - - constructor(type: ModifierType, pokemonId: number, battlesLeft?: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.battlesLeft = battlesLeft!; // TODO: is this bang correct? - } - - /** - * Lapse the {@linkcode battlesLeft} counter (reduce it by 1) - * @param _args arguments passed (not used here) - * @returns `true` if {@linkcode battlesLeft} is not null - */ - public lapse(..._args: unknown[]): boolean { - return !!--this.battlesLeft; - } - - /** - * Retrieve the {@linkcode Modifier | Modifiers} icon as a {@linkcode Phaser.GameObjects.Container | Container} - * @param forSummary `true` if the icon is for the summary screen - * @returns the icon as a {@linkcode Phaser.GameObjects.Container | Container} - */ - public getIcon(forSummary?: boolean): Phaser.GameObjects.Container { - const container = super.getIcon(forSummary); - - if (this.getPokemon()?.isPlayer()) { - const battleCountText = addTextObject(27, 0, this.battlesLeft.toString(), TextStyle.PARTY, { - fontSize: "66px", - color: Color.PINK, - }); - battleCountText.setShadow(0, 0); - battleCountText.setStroke(ShadowColor.RED, 16); - battleCountText.setOrigin(1, 0); - container.add(battleCountText); - } - - return container; - } - - getBattlesLeft(): number { - return this.battlesLeft; - } - - getMaxStackCount(_forThreshold?: boolean): number { - return 1; - } -} - -/** - * Modifier used for held items, specifically vitamins like Carbos, Hp Up, etc., that - * increase the value of a given {@linkcode PermanentStat}. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class BaseStatModifier extends PokemonHeldItemModifier { - protected stat: PermanentStat; - public isTransferable = false; - - constructor(type: ModifierType, pokemonId: number, stat: PermanentStat, stackCount?: number) { - super(type, pokemonId, stackCount); - this.stat = stat; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof BaseStatModifier) { - return (modifier as BaseStatModifier).stat === this.stat; - } - return false; - } - - clone(): PersistentModifier { - return new BaseStatModifier(this.type, this.pokemonId, this.stat, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(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` - */ - override apply(_pokemon: Pokemon, baseStats: number[]): boolean { - baseStats[this.stat] = Math.floor(baseStats[this.stat] * (1 + this.getStackCount() * 0.1)); - return true; - } - - getScoreMultiplier(): number { - return 1.1; - } - - getMaxHeldItemCount(pokemon: Pokemon): number { - return pokemon.ivs[this.stat]; - } -} - -export class EvoTrackerModifier extends PokemonHeldItemModifier { - protected species: SpeciesId; - protected required: number; - public isTransferable = false; - - constructor(type: ModifierType, pokemonId: number, species: SpeciesId, required: number, stackCount?: number) { - super(type, pokemonId, stackCount); - this.species = species; - this.required = required; - } - - matchType(modifier: Modifier): boolean { - return ( - modifier instanceof EvoTrackerModifier && modifier.species === this.species && modifier.required === this.required - ); - } - - clone(): PersistentModifier { - return new EvoTrackerModifier(this.type, this.pokemonId, this.species, this.required, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat([this.species, this.required]); - } - - /** - * Applies the {@linkcode EvoTrackerModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getIconStackText(_virtual?: boolean): Phaser.GameObjects.BitmapText | null { - const pokemon = this.getPokemon(); - - const count = (pokemon?.getPersistentTreasureCount() || 0) + this.getStackCount(); - - const text = globalScene.add.bitmapText(10, 15, "item-count", count.toString(), 11); - text.letterSpacing = -0.5; - if (count >= this.required) { - text.setTint(0xf89890); - } - text.setOrigin(0, 0); - - return text; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 999; - } - - override getSpecies(): SpeciesId { - return this.species; - } -} - -/** - * Currently used by Shuckle Juice item - */ -export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { - public override type: PokemonBaseStatTotalModifierType; - public isTransferable = false; - public statModifier: 10 | -15; - - constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: 10 | -15, stackCount?: number) { - super(type, pokemonId, stackCount); - this.statModifier = statModifier; - } - - override matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonBaseStatTotalModifier && this.statModifier === modifier.statModifier; - } - - override clone(): PersistentModifier { - return new PokemonBaseStatTotalModifier(this.type, this.pokemonId, this.statModifier, this.stackCount); - } - - override getArgs(): any[] { - return super.getArgs().concat(this.statModifier); - } - - /** - * Checks if {@linkcode PokemonBaseStatTotalModifier} 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 PokemonBaseStatTotalModifier} - * @param _pokemon the {@linkcode Pokemon} to be modified - * @param baseStats the base stats of the {@linkcode Pokemon} - * @returns always `true` - */ - override apply(_pokemon: Pokemon, baseStats: number[]): boolean { - // Modifies the passed in baseStats[] array - baseStats.forEach((v, i) => { - // HP is affected by half as much as other stats - const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier); - baseStats[i] = Math.min(Math.max(newVal, 1), 999999); - }); - - return true; - } - - override getScoreMultiplier(): number { - return 1.2; - } - - override getMaxHeldItemCount(_pokemon: Pokemon): number { - return 2; - } -} - -/** - * Currently used by Old Gateau item - */ -export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { - public isTransferable = false; - - override matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonBaseStatFlatModifier; - } - - override clone(): PersistentModifier { - return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if the {@linkcode PokemonBaseStatFlatModifier} should be applied to the {@linkcode Pokemon}. - * @param pokemon The {@linkcode Pokemon} that holds the item - * @param baseStats The base stats of the {@linkcode Pokemon} - * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { - return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); - } - - /** - * Applies the {@linkcode PokemonBaseStatFlatModifier} - * @param _pokemon The {@linkcode Pokemon} that holds the item - * @param baseStats The base stats of the {@linkcode Pokemon} - * @returns always `true` - */ - override apply(pokemon: Pokemon, baseStats: number[]): boolean { - // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats - const stats = this.getStats(pokemon); - const statModifier = 20; - baseStats.forEach((v, i) => { - if (stats.includes(i)) { - const newVal = Math.floor(v + statModifier); - baseStats[i] = Math.min(Math.max(newVal, 1), 999999); - } - }); - - return true; - } - - /** - * Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef - * @returns Array of 3 {@linkcode Stat}s to boost - */ - getStats(pokemon: Pokemon): Stat[] { - const stats: Stat[] = []; - const baseStats = pokemon.getSpeciesForm().baseStats.slice(0); - // HP or Speed - stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD); - // Attack or SpAtk - stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); - // Def or SpDef - stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); - return stats; - } - - override getScoreMultiplier(): number { - return 1.1; - } - - override getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Currently used by Macho Brace item - */ -export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { - public isTransferable = false; - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonIncrementingStatModifier; - } - - clone(): PokemonIncrementingStatModifier { - return new PokemonIncrementingStatModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if the {@linkcode PokemonIncrementingStatModifier} should be applied to the {@linkcode Pokemon}. - * @param pokemon The {@linkcode Pokemon} that holds the item - * @param stat The affected {@linkcode Stat} - * @param statHolder The {@linkcode NumberHolder} that holds the stat - * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, stat?: Stat, statHolder?: NumberHolder): boolean { - return super.shouldApply(pokemon, stat, statHolder) && !!statHolder; - } - - /** - * Applies the {@linkcode PokemonIncrementingStatModifier} - * @param _pokemon The {@linkcode Pokemon} that holds the item - * @param stat The affected {@linkcode Stat} - * @param statHolder The {@linkcode NumberHolder} that holds the stat - * @returns always `true` - */ - override apply(_pokemon: Pokemon, stat: Stat, statHolder: NumberHolder): boolean { - // Modifies the passed in stat number holder by +2 per stack for HP, +1 per stack for other stats - // If the Macho Brace is at max stacks (50), adds additional 10% to total HP and 5% to other stats - const isHp = stat === Stat.HP; - - if (isHp) { - statHolder.value += 2 * this.stackCount; - if (this.stackCount === this.getMaxHeldItemCount()) { - statHolder.value = Math.floor(statHolder.value * 1.1); - } - } else { - statHolder.value += this.stackCount; - if (this.stackCount === this.getMaxHeldItemCount()) { - statHolder.value = Math.floor(statHolder.value * 1.05); - } - } - - return true; - } - - getScoreMultiplier(): number { - return 1.2; - } - - getMaxHeldItemCount(_pokemon?: Pokemon): number { - return 50; - } -} - -/** - * Modifier used for held items that Applies {@linkcode Stat} boost(s) - * using a multiplier. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class StatBoosterModifier extends PokemonHeldItemModifier { - /** The stats that the held item boosts */ - protected stats: Stat[]; - /** The multiplier used to increase the relevant stat(s) */ - protected multiplier: number; - - constructor(type: ModifierType, pokemonId: number, stats: Stat[], multiplier: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.stats = stats; - this.multiplier = multiplier; - } - - clone() { - return new StatBoosterModifier(this.type, this.pokemonId, this.stats, this.multiplier, this.stackCount); - } - - getArgs(): any[] { - return [...super.getArgs(), this.stats, this.multiplier]; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof StatBoosterModifier) { - const modifierInstance = modifier as StatBoosterModifier; - if (modifierInstance.multiplier === this.multiplier && modifierInstance.stats.length === this.stats.length) { - return modifierInstance.stats.every((e, i) => e === this.stats[i]); - } - } - - return false; - } - - /** - * Checks if the incoming stat is listed in {@linkcode stats} - * @param _pokemon the {@linkcode Pokemon} that holds the item - * @param _stat the {@linkcode Stat} to be boosted - * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat could be boosted, false otherwise - */ - override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { - return super.shouldApply(pokemon, stat, statValue) && this.stats.includes(stat); - } - - /** - * Boosts the incoming stat by a {@linkcode multiplier} if the stat is listed - * in {@linkcode stats}. - * @param _pokemon the {@linkcode Pokemon} that holds the item - * @param _stat the {@linkcode Stat} to be boosted - * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat boost applies successfully, false otherwise - * @see shouldApply - */ - override apply(_pokemon: Pokemon, _stat: Stat, statValue: NumberHolder): boolean { - statValue.value *= this.multiplier; - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Modifier used for held items, specifically Eviolite, that apply - * {@linkcode Stat} boost(s) using a multiplier if the holder can evolve. - * @extends StatBoosterModifier - * @see {@linkcode apply} - */ -export class EvolutionStatBoosterModifier extends StatBoosterModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof EvolutionStatBoosterModifier; - } - - /** - * Checks if the stat boosts can apply and if the holder is not currently - * Gigantamax'd. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param stat {@linkcode Stat} The {@linkcode Stat} to be boosted - * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat boosts can be applied, false otherwise - */ - override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { - return super.shouldApply(pokemon, stat, statValue) && !pokemon.isMax(); - } - - /** - * Boosts the incoming stat value by a {@linkcode EvolutionStatBoosterModifier.multiplier} if the holder - * can evolve. Note that, if the holder is a fusion, they will receive - * only half of the boost if either of the fused members are fully - * evolved. However, if they are both unevolved, the full boost - * will apply. - * @param pokemon {@linkcode Pokemon} that holds the item - * @param _stat {@linkcode Stat} The {@linkcode Stat} to be boosted - * @param statValue{@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat boost applies successfully, false otherwise - * @see shouldApply - */ - override apply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { - const isUnevolved = pokemon.getSpeciesForm(true).speciesId in pokemonEvolutions; - - if (pokemon.isFusion() && pokemon.getFusionSpeciesForm(true).speciesId in pokemonEvolutions !== isUnevolved) { - // Half boost applied if pokemon is fused and either part of fusion is fully evolved - statValue.value *= 1 + (this.multiplier - 1) / 2; - return true; - } - if (isUnevolved) { - // Full boost applied if holder is unfused and unevolved or, if fused, both parts of fusion are unevolved - return super.apply(pokemon, stat, statValue); - } - - return false; - } -} - -/** - * Modifier used for held items that Applies {@linkcode Stat} boost(s) using a - * multiplier if the holder is of a specific {@linkcode SpeciesId}. - * @extends StatBoosterModifier - * @see {@linkcode apply} - */ -export class SpeciesStatBoosterModifier extends StatBoosterModifier { - /** The species that the held item's stat boost(s) apply to */ - private species: SpeciesId[]; - - constructor( - type: ModifierType, - pokemonId: number, - stats: Stat[], - multiplier: number, - species: SpeciesId[], - stackCount?: number, - ) { - super(type, pokemonId, stats, multiplier, stackCount); - - this.species = species; - } - - clone() { - return new SpeciesStatBoosterModifier( - this.type, - this.pokemonId, - this.stats, - this.multiplier, - this.species, - this.stackCount, - ); - } - - getArgs(): any[] { - return [...super.getArgs(), this.species]; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof SpeciesStatBoosterModifier) { - const modifierInstance = modifier as SpeciesStatBoosterModifier; - if (modifierInstance.species.length === this.species.length) { - return super.matchType(modifier) && modifierInstance.species.every((e, i) => e === this.species[i]); - } - } - - return false; - } - - /** - * Checks if the incoming stat is listed in {@linkcode stats} and if the holder's {@linkcode SpeciesId} - * (or its fused species) is listed in {@linkcode species}. - * @param pokemon {@linkcode Pokemon} that holds the item - * @param stat {@linkcode Stat} being checked at the time - * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat could be boosted, false otherwise - */ - override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { - return ( - super.shouldApply(pokemon, stat, statValue) && - (this.species.includes(pokemon.getSpeciesForm(true).speciesId) || - (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId))) - ); - } - - /** - * Checks if either parameter is included in the corresponding lists - * @param speciesId {@linkcode SpeciesId} being checked - * @param stat {@linkcode Stat} being checked - * @returns `true` if both parameters are in {@linkcode species} and {@linkcode stats} respectively, false otherwise - */ - contains(speciesId: SpeciesId, stat: Stat): boolean { - return this.species.includes(speciesId) && this.stats.includes(stat); - } -} - -/** - * Modifier used for held items that apply critical-hit stage boost(s). - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class CritBoosterModifier extends PokemonHeldItemModifier { - /** The amount of stages by which the held item increases the current critical-hit stage value */ - protected stageIncrement: number; - - constructor(type: ModifierType, pokemonId: number, stageIncrement: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.stageIncrement = stageIncrement; - } - - clone() { - return new CritBoosterModifier(this.type, this.pokemonId, this.stageIncrement, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.stageIncrement); - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof CritBoosterModifier) { - return (modifier as CritBoosterModifier).stageIncrement === this.stageIncrement; - } - - return false; - } - - /** - * Increases the current critical-hit stage value by {@linkcode stageIncrement}. - * @param _pokemon {@linkcode Pokemon} N/A - * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns always `true` - */ - override apply(_pokemon: Pokemon, critStage: NumberHolder): boolean { - critStage.value += this.stageIncrement; - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Modifier used for held items that apply critical-hit stage boost(s) - * if the holder is of a specific {@linkcode SpeciesId}. - * @extends CritBoosterModifier - * @see {@linkcode shouldApply} - */ -export class SpeciesCritBoosterModifier extends CritBoosterModifier { - /** The species that the held item's critical-hit stage boost applies to */ - private species: SpeciesId[]; - - constructor( - type: ModifierType, - pokemonId: number, - stageIncrement: number, - species: SpeciesId[], - stackCount?: number, - ) { - super(type, pokemonId, stageIncrement, stackCount); - - this.species = species; - } - - clone() { - return new SpeciesCritBoosterModifier( - this.type, - this.pokemonId, - this.stageIncrement, - this.species, - this.stackCount, - ); - } - - getArgs(): any[] { - return [...super.getArgs(), this.species]; - } - - matchType(modifier: Modifier): boolean { - return modifier instanceof SpeciesCritBoosterModifier; - } - - /** - * Checks if the holder's {@linkcode SpeciesId} (or its fused species) is listed - * in {@linkcode species}. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns `true` if the critical-hit level can be incremented, false otherwise - */ - override shouldApply(pokemon: Pokemon, critStage: NumberHolder): boolean { - return ( - super.shouldApply(pokemon, critStage) && - (this.species.includes(pokemon.getSpeciesForm(true).speciesId) || - (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId))) - ); - } -} - -/** - * Applies Specific Type item boosts (e.g., Magnet) - */ -export class AttackTypeBoosterModifier extends PokemonHeldItemModifier { - public moveType: PokemonType; - private boostMultiplier: number; - - constructor(type: ModifierType, pokemonId: number, moveType: PokemonType, boostPercent: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.moveType = moveType; - this.boostMultiplier = boostPercent * 0.01; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof AttackTypeBoosterModifier) { - const attackTypeBoosterModifier = modifier as AttackTypeBoosterModifier; - return ( - attackTypeBoosterModifier.moveType === this.moveType && - attackTypeBoosterModifier.boostMultiplier === this.boostMultiplier - ); - } - - return false; - } - - clone() { - return new AttackTypeBoosterModifier( - this.type, - this.pokemonId, - this.moveType, - this.boostMultiplier * 100, - this.stackCount, - ); - } - - getArgs(): any[] { - return super.getArgs().concat([this.moveType, this.boostMultiplier * 100]); - } - - /** - * Checks if {@linkcode AttackTypeBoosterModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the held item - * @param moveType the {@linkcode PokemonType} of the move being used - * @param movePower the {@linkcode NumberHolder} that holds the power of the move - * @returns `true` if boosts should be applied to the move. - */ - override shouldApply(pokemon?: Pokemon, moveType?: PokemonType, movePower?: NumberHolder): boolean { - return ( - super.shouldApply(pokemon, moveType, movePower) && - typeof moveType === "number" && - movePower instanceof NumberHolder && - this.moveType === moveType - ); - } - - /** - * Applies {@linkcode AttackTypeBoosterModifier} - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param moveType {@linkcode PokemonType} of the move being used - * @param movePower {@linkcode NumberHolder} that holds the power of the move - * @returns `true` if boosts have been applied to the move. - */ - override apply(_pokemon: Pokemon, moveType: PokemonType, movePower: NumberHolder): boolean { - if (moveType === this.moveType && movePower.value >= 1) { - (movePower as NumberHolder).value = Math.floor( - (movePower as NumberHolder).value * (1 + this.getStackCount() * this.boostMultiplier), - ); - return true; - } - - return false; - } - - getScoreMultiplier(): number { - return 1.2; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 99; - } -} - -export class SurviveDamageModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof SurviveDamageModifier; - } - - clone() { - return new SurviveDamageModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if the {@linkcode SurviveDamageModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage - * @returns `true` if the {@linkcode SurviveDamageModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, surviveDamage?: BooleanHolder): boolean { - return super.shouldApply(pokemon, surviveDamage) && !!surviveDamage; - } - - /** - * Applies {@linkcode SurviveDamageModifier} - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage - * @returns `true` if the survive damage has been applied - */ - override apply(pokemon: Pokemon, surviveDamage: BooleanHolder): boolean { - if (!surviveDamage.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - surviveDamage.value = true; - - globalScene.phaseManager.queueMessage( - i18next.t("modifier:surviveDamageApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - ); - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 5; - } -} - -export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof BypassSpeedChanceModifier; - } - - clone() { - return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if {@linkcode BypassSpeedChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { - return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; - } - - /** - * Applies {@linkcode BypassSpeedChanceModifier} - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied - */ - override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean { - if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - doBypassSpeed.value = true; - const isCommandFight = - globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; - const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW"; - - if (isCommandFight && hasQuickClaw) { - globalScene.phaseManager.queueMessage( - i18next.t("modifier:bypassSpeedChanceApply", { - pokemonName: getPokemonNameWithAffix(pokemon), - itemName: i18next.t("modifierType:ModifierType.QUICK_CLAW.name"), - }), - ); - } - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -/** - * Class for Pokemon held items like King's Rock - * Because King's Rock can be stacked in PokeRogue, unlike mainline, it does not receive a boost from AbilityId.SERENE_GRACE - */ -export class FlinchChanceModifier extends PokemonHeldItemModifier { - private chance: number; - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.chance = 10; - } - - matchType(modifier: Modifier) { - return modifier instanceof FlinchChanceModifier; - } - - clone() { - return new FlinchChanceModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if {@linkcode FlinchChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param flinched {@linkcode BooleanHolder} that is `true` if the pokemon flinched - * @returns `true` if {@linkcode FlinchChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, flinched?: BooleanHolder): boolean { - return super.shouldApply(pokemon, flinched) && !!flinched; - } - - /** - * Applies {@linkcode FlinchChanceModifier} to randomly flinch targets hit. - * @param pokemon - The {@linkcode Pokemon} that holds the item - * @param flinched - A {@linkcode BooleanHolder} holding whether the pokemon has flinched - * @returns `true` if {@linkcode FlinchChanceModifier} was applied successfully - */ - override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean { - // The check for pokemon.summonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch - // TODO: Since summonData is always defined now, we can probably remove this - if (pokemon.summonData && !flinched.value && pokemon.randBattleSeedInt(100) < this.getStackCount() * this.chance) { - flinched.value = true; - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -export class TurnHealModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof TurnHealModifier; - } - - clone() { - return new TurnHealModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode TurnHealModifier} - * @param pokemon The {@linkcode Pokemon} that holds the item - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(pokemon: Pokemon): boolean { - if (!pokemon.isFullHp()) { - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - pokemon.getBattlerIndex(), - toDmgValue(pokemon.getMaxHp() / 16) * this.stackCount, - i18next.t("modifier:turnHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - true, - ); - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 4; - } -} - -/** - * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a - * set {@linkcode StatusEffect} at the end of a turn. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class TurnStatusEffectModifier extends PokemonHeldItemModifier { - /** The status effect to be applied by the held item */ - private effect: StatusEffect; - - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - switch (type.id) { - case "TOXIC_ORB": - this.effect = StatusEffect.TOXIC; - break; - case "FLAME_ORB": - this.effect = StatusEffect.BURN; - break; - } - } - - /** - * Checks if {@linkcode modifier} is an instance of this class, - * intentionally ignoring potentially different {@linkcode effect}s - * to prevent held item stockpiling since the item obtained first - * would be the only item able to {@linkcode apply} successfully. - * @override - * @param modifier {@linkcode Modifier} being type tested - * @return `true` if {@linkcode modifier} is an instance of - * TurnStatusEffectModifier, false otherwise - */ - matchType(modifier: Modifier): boolean { - return modifier instanceof TurnStatusEffectModifier; - } - - clone() { - return new TurnStatusEffectModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Tries to inflicts the holder with the associated {@linkcode StatusEffect}. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @returns `true` if the status effect was applied successfully - */ - override apply(pokemon: Pokemon): boolean { - return pokemon.trySetStatus(this.effect, true, undefined, undefined, this.type.name); - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } - - getStatusEffect(): StatusEffect { - return this.effect; - } -} - -export class HitHealModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof HitHealModifier; - } - - clone() { - return new HitHealModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode HitHealModifier} - * @param pokemon The {@linkcode Pokemon} that holds the item - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(pokemon: Pokemon): boolean { - if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) { - // TODO: this shouldn't be undefined AFAIK - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - pokemon.getBattlerIndex(), - toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, - i18next.t("modifier:hitHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - true, - ); - } - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 4; - } -} - -export class LevelIncrementBoosterModifier extends PersistentModifier { - match(modifier: Modifier) { - return modifier instanceof LevelIncrementBoosterModifier; - } - - clone() { - return new LevelIncrementBoosterModifier(this.type, this.stackCount); - } - - /** - * Checks if {@linkcode LevelIncrementBoosterModifier} should be applied - * @param count {@linkcode NumberHolder} holding the level increment count - * @returns `true` if {@linkcode LevelIncrementBoosterModifier} should be applied - */ - override shouldApply(count: NumberHolder): boolean { - return !!count; - } - - /** - * Applies {@linkcode LevelIncrementBoosterModifier} - * @param count {@linkcode NumberHolder} holding the level increment count - * @returns always `true` - */ - override apply(count: NumberHolder): boolean { - count.value += this.getStackCount(); - - return true; - } - - getMaxStackCount(_forThreshold?: boolean): number { - return 99; - } -} - -export class BerryModifier extends PokemonHeldItemModifier { - public berryType: BerryType; - public consumed: boolean; - - constructor(type: ModifierType, pokemonId: number, berryType: BerryType, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.berryType = berryType; - this.consumed = false; - } - - matchType(modifier: Modifier) { - return modifier instanceof BerryModifier && (modifier as BerryModifier).berryType === this.berryType; - } - - clone() { - return new BerryModifier(this.type, this.pokemonId, this.berryType, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.berryType); - } - - /** - * 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 BerryModifier} - * @param pokemon The {@linkcode Pokemon} that holds the berry - * @returns always `true` - */ - override apply(pokemon: Pokemon): boolean { - const preserve = new BooleanHolder(false); - globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); - this.consumed = !preserve.value; - - // munch the berry and trigger unburden-like effects - getBerryEffectFunc(this.berryType)(pokemon); - applyAbAttrs("PostItemLostAbAttr", { pokemon }); - - // 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, this.consumed); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - if ([BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(this.berryType)) { - return 2; - } - return 3; - } -} - -export class PreserveBerryModifier extends PersistentModifier { - match(modifier: Modifier) { - return modifier instanceof PreserveBerryModifier; - } - - clone() { - return new PreserveBerryModifier(this.type, this.stackCount); - } - - /** - * Checks if all prequired conditions are met to apply {@linkcode PreserveBerryModifier} - * @param pokemon {@linkcode Pokemon} that holds the berry - * @param doPreserve {@linkcode BooleanHolder} that is `true` if the berry should be preserved - * @returns `true` if {@linkcode PreserveBerryModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doPreserve?: BooleanHolder): boolean { - return !!pokemon && !!doPreserve; - } - - /** - * Applies {@linkcode PreserveBerryModifier} - * @param pokemon The {@linkcode Pokemon} that holds the berry - * @param doPreserve {@linkcode BooleanHolder} that is `true` if the berry should be preserved - * @returns always `true` - */ - override apply(pokemon: Pokemon, doPreserve: BooleanHolder): boolean { - doPreserve.value ||= pokemon.randBattleSeedInt(10) < this.getStackCount() * 3; - - return true; - } - - getMaxStackCount(): number { - return 3; - } -} - -export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof PokemonInstantReviveModifier; - } - - clone() { - return new PokemonInstantReviveModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode PokemonInstantReviveModifier} - * @param pokemon The {@linkcode Pokemon} that holds the item - * @returns always `true` - */ - override apply(pokemon: Pokemon): boolean { - // Restore the Pokemon to half HP - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - pokemon.getBattlerIndex(), - toDmgValue(pokemon.getMaxHp() / 2), - i18next.t("modifier:pokemonInstantReviveApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - false, - false, - true, - ); - - // Remove the Pokemon's FAINT status - pokemon.resetStatus(true, false, true, false); - - // Reapply Commander on the Pokemon's side of the field, if applicable - const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - for (const p of field) { - applyAbAttrs("CommanderAbAttr", { pokemon: p }); - } - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Modifier used for held items, namely White Herb, that restore adverse stat - * stages in battle. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof ResetNegativeStatStageModifier; - } - - clone() { - return new ResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Goes through the holder's stat stages and, if any are negative, resets that - * stat stage back to 0. - * @param pokemon {@linkcode Pokemon} that holds the item - * @returns `true` if any stat stages were reset, false otherwise - */ - override apply(pokemon: Pokemon): boolean { - let statRestored = false; - - for (const s of BATTLE_STATS) { - if (pokemon.getStatStage(s) < 0) { - pokemon.setStatStage(s, 0); - statRestored = true; - } - } - - if (statRestored) { - globalScene.phaseManager.queueMessage( - i18next.t("modifier:resetNegativeStatStageApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - ); - } - return statRestored; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 2; - } -} - -/** - * Modifier used for held items, namely Mystical Rock, that extend the - * duration of weather and terrain effects. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class FieldEffectModifier extends PokemonHeldItemModifier { - /** - * Provides two more turns per stack to any weather or terrain effect caused - * by the holder. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param fieldDuration {@linkcode NumberHolder} that stores the current field effect duration - * @returns `true` if the field effect extension was applied successfully - */ - override apply(_pokemon: Pokemon, fieldDuration: NumberHolder): boolean { - fieldDuration.value += 2 * this.stackCount; - return true; - } - - override matchType(modifier: Modifier): boolean { - return modifier instanceof FieldEffectModifier; - } - - override clone(): FieldEffectModifier { - return new FieldEffectModifier(this.type, this.pokemonId, this.stackCount); - } - - override getMaxHeldItemCount(_pokemon?: Pokemon): number { - return 2; - } -} - export abstract class ConsumablePokemonModifier extends ConsumableModifier { public pokemonId: number; @@ -2310,7 +383,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier { * @returns always `true` */ override apply(playerPokemon: PlayerPokemon, levelCount: NumberHolder = new NumberHolder(1)): boolean { - globalScene.applyModifiers(LevelIncrementBoosterModifier, true, levelCount); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.LEVEL_INCREMENT_BOOSTER, { numberHolder: levelCount }); playerPokemon.level += levelCount.value; if (playerPokemon.level <= globalScene.getMaxExpLevel(true)) { @@ -2443,455 +516,6 @@ export class FusePokemonModifier extends ConsumablePokemonModifier { } } -export class MultipleParticipantExpBonusModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof MultipleParticipantExpBonusModifier; - } - - /** - * Applies {@linkcode MultipleParticipantExpBonusModifier} - * @returns always `true` - */ - apply(): boolean { - return true; - } - - clone(): MultipleParticipantExpBonusModifier { - return new MultipleParticipantExpBonusModifier(this.type, this.stackCount); - } - - getMaxStackCount(): number { - return 5; - } -} - -export class HealingBoosterModifier extends PersistentModifier { - private multiplier: number; - - constructor(type: ModifierType, multiplier: number, stackCount?: number) { - super(type, stackCount); - - this.multiplier = multiplier; - } - - match(modifier: Modifier): boolean { - return modifier instanceof HealingBoosterModifier; - } - - clone(): HealingBoosterModifier { - return new HealingBoosterModifier(this.type, this.multiplier, this.stackCount); - } - - getArgs(): any[] { - return [this.multiplier]; - } - - /** - * Applies {@linkcode HealingBoosterModifier} - * @param healingMultiplier the multiplier to apply to the healing - * @returns always `true` - */ - override apply(healingMultiplier: NumberHolder): boolean { - healingMultiplier.value *= 1 + (this.multiplier - 1) * this.getStackCount(); - - return true; - } - - getMaxStackCount(): number { - return 5; - } -} - -export class ExpBoosterModifier extends PersistentModifier { - private boostMultiplier: number; - - constructor(type: ModifierType, boostPercent: number, stackCount?: number) { - super(type, stackCount); - - this.boostMultiplier = boostPercent * 0.01; - } - - match(modifier: Modifier): boolean { - if (modifier instanceof ExpBoosterModifier) { - const expModifier = modifier as ExpBoosterModifier; - return expModifier.boostMultiplier === this.boostMultiplier; - } - return false; - } - - clone(): ExpBoosterModifier { - return new ExpBoosterModifier(this.type, this.boostMultiplier * 100, this.stackCount); - } - - getArgs(): any[] { - return [this.boostMultiplier * 100]; - } - - /** - * Applies {@linkcode ExpBoosterModifier} - * @param boost {@linkcode NumberHolder} holding the boost value - * @returns always `true` - */ - override apply(boost: NumberHolder): boolean { - boost.value = Math.floor(boost.value * (1 + this.getStackCount() * this.boostMultiplier)); - - return true; - } - - getMaxStackCount(_forThreshold?: boolean): number { - return this.boostMultiplier < 1 ? (this.boostMultiplier < 0.6 ? 99 : 30) : 10; - } -} - -export class PokemonExpBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonExpBoosterModifierType; - - private boostMultiplier: number; - - constructor(type: PokemonExpBoosterModifierType, pokemonId: number, boostPercent: number, stackCount?: number) { - super(type, pokemonId, stackCount); - this.boostMultiplier = boostPercent * 0.01; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof PokemonExpBoosterModifier) { - const pokemonExpModifier = modifier as PokemonExpBoosterModifier; - return pokemonExpModifier.boostMultiplier === this.boostMultiplier; - } - return false; - } - - clone(): PersistentModifier { - return new PokemonExpBoosterModifier(this.type, this.pokemonId, this.boostMultiplier * 100, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.boostMultiplier * 100); - } - - /** - * Checks if {@linkcode PokemonExpBoosterModifier} should be applied - * @param pokemon The {@linkcode Pokemon} to apply the exp boost to - * @param boost {@linkcode NumberHolder} holding the exp boost value - * @returns `true` if {@linkcode PokemonExpBoosterModifier} should be applied - */ - override shouldApply(pokemon: Pokemon, boost: NumberHolder): boolean { - return super.shouldApply(pokemon, boost) && !!boost; - } - - /** - * Applies {@linkcode PokemonExpBoosterModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the exp boost to - * @param boost {@linkcode NumberHolder} holding the exp boost value - * @returns always `true` - */ - override apply(_pokemon: Pokemon, boost: NumberHolder): boolean { - boost.value = Math.floor(boost.value * (1 + this.getStackCount() * this.boostMultiplier)); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 99; - } -} - -export class ExpShareModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof ExpShareModifier; - } - - clone(): ExpShareModifier { - return new ExpShareModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode ExpShareModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxStackCount(): number { - return 5; - } -} - -export class ExpBalanceModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof ExpBalanceModifier; - } - - clone(): ExpBalanceModifier { - return new ExpBalanceModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode ExpBalanceModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxStackCount(): number { - return 4; - } -} - -export class PokemonFriendshipBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonFriendshipBoosterModifierType; - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonFriendshipBoosterModifier; - } - - clone(): PersistentModifier { - return new PokemonFriendshipBoosterModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode PokemonFriendshipBoosterModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the friendship boost to - * @param friendship {@linkcode NumberHolder} holding the friendship boost value - * @returns always `true` - */ - override apply(_pokemon: Pokemon, friendship: NumberHolder): boolean { - friendship.value = Math.floor(friendship.value * (1 + 0.5 * this.getStackCount())); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -export class PokemonNatureWeightModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonNatureWeightModifier; - } - - clone(): PersistentModifier { - return new PokemonNatureWeightModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode PokemonNatureWeightModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the nature weight to - * @param multiplier {@linkcode NumberHolder} holding the nature weight - * @returns `true` if multiplier was applied - */ - override apply(_pokemon: Pokemon, multiplier: NumberHolder): boolean { - if (multiplier.value !== 1) { - multiplier.value += 0.1 * this.getStackCount() * (multiplier.value > 1 ? 1 : -1); - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 10; - } -} - -export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonMoveAccuracyBoosterModifierType; - private accuracyAmount: number; - - constructor(type: PokemonMoveAccuracyBoosterModifierType, pokemonId: number, accuracy: number, stackCount?: number) { - super(type, pokemonId, stackCount); - this.accuracyAmount = accuracy; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof PokemonMoveAccuracyBoosterModifier) { - const pokemonAccuracyBoosterModifier = modifier as PokemonMoveAccuracyBoosterModifier; - return pokemonAccuracyBoosterModifier.accuracyAmount === this.accuracyAmount; - } - return false; - } - - clone(): PersistentModifier { - return new PokemonMoveAccuracyBoosterModifier(this.type, this.pokemonId, this.accuracyAmount, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.accuracyAmount); - } - - /** - * Checks if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied - * @param pokemon The {@linkcode Pokemon} to apply the move accuracy boost to - * @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost - * @returns `true` if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, moveAccuracy?: NumberHolder): boolean { - return super.shouldApply(pokemon, moveAccuracy) && !!moveAccuracy; - } - - /** - * Applies {@linkcode PokemonMoveAccuracyBoosterModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the move accuracy boost to - * @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost - * @returns always `true` - */ - override apply(_pokemon: Pokemon, moveAccuracy: NumberHolder): boolean { - moveAccuracy.value = moveAccuracy.value + this.accuracyAmount * this.getStackCount(); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -export class PokemonMultiHitModifier extends PokemonHeldItemModifier { - public override type: PokemonMultiHitModifierType; - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonMultiHitModifier; - } - - clone(): PersistentModifier { - return new PokemonMultiHitModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * For each stack, converts 25 percent of attack damage into an additional strike. - * @param pokemon The {@linkcode Pokemon} using the move - * @param moveId The {@linkcode MoveId | identifier} for the move being used - * @param count {@linkcode NumberHolder} holding the move's hit count for this turn - * @param damageMultiplier {@linkcode NumberHolder} holding a damage multiplier applied to a strike of this move - * @returns always `true` - */ - override apply( - pokemon: Pokemon, - moveId: MoveId, - count: NumberHolder | null = null, - damageMultiplier: NumberHolder | null = null, - ): boolean { - const move = allMoves[moveId]; - /** - * The move must meet Parental Bond's restrictions for this item - * to apply. This means - * - Only attacks are boosted - * - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted - * (though Multi-Lens can still affect moves boosted by Parental Bond) - * - Multi-target moves are not boosted *unless* they can only hit a single Pokemon - * - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted - */ - if (!move.canBeMultiStrikeEnhanced(pokemon)) { - return false; - } - - if (!isNullOrUndefined(count)) { - return this.applyHitCountBoost(count); - } - if (!isNullOrUndefined(damageMultiplier)) { - return this.applyDamageModifier(pokemon, damageMultiplier); - } - - return false; - } - - /** Adds strikes to a move equal to the number of stacked Multi-Lenses */ - private applyHitCountBoost(count: NumberHolder): boolean { - count.value += this.getStackCount(); - return true; - } - - /** - * If applied to the first hit of a move, sets the damage multiplier - * equal to (1 - the number of stacked Multi-Lenses). - * Additional strikes beyond that are given a 0.25x damage multiplier - */ - private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { - if (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) { - // Reduce first hit by 25% for each stack count - damageMultiplier.value *= 1 - 0.25 * this.getStackCount(); - return true; - } - if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== this.getStackCount() + 1) { - // Deal 25% damage for each remaining Multi Lens hit - damageMultiplier.value *= 0.25; - return true; - } - // An extra hit not caused by Multi Lens -- assume it is Parental Bond - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 2; - } -} - -export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier { - public override type: FormChangeItemModifierType; - public formChangeItem: FormChangeItem; - public active: boolean; - public isTransferable = false; - - constructor( - type: FormChangeItemModifierType, - pokemonId: number, - formChangeItem: FormChangeItem, - active: boolean, - stackCount?: number, - ) { - super(type, pokemonId, stackCount); - this.formChangeItem = formChangeItem; - this.active = active; - } - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonFormChangeItemModifier && modifier.formChangeItem === this.formChangeItem; - } - - clone(): PersistentModifier { - return new PokemonFormChangeItemModifier( - this.type, - this.pokemonId, - this.formChangeItem, - this.active, - this.stackCount, - ); - } - - getArgs(): any[] { - return super.getArgs().concat(this.formChangeItem, this.active); - } - - /** - * Applies {@linkcode PokemonFormChangeItemModifier} - * @param pokemon The {@linkcode Pokemon} to apply the form change item to - * @param active `true` if the form change item is active - * @returns `true` if the form change item was applied - */ - override apply(pokemon: Pokemon, active: boolean): boolean { - const switchActive = this.active && !active; - - if (switchActive) { - this.active = false; - } - - const ret = globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger); - - if (switchActive) { - this.active = true; - } - - return ret; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - export class MoneyRewardModifier extends ConsumableModifier { private moneyMultiplier: number; @@ -2908,855 +532,19 @@ export class MoneyRewardModifier extends ConsumableModifier { override apply(): boolean { const moneyAmount = new NumberHolder(globalScene.getWaveMoneyAmount(this.moneyMultiplier)); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); globalScene.addMoney(moneyAmount.value); - globalScene.getPlayerParty().map(p => { + for (const p of globalScene.getPlayerParty()) { if (p.species?.speciesId === SpeciesId.GIMMIGHOUL || p.fusionSpecies?.speciesId === SpeciesId.GIMMIGHOUL) { const factor = Math.min(Math.floor(this.moneyMultiplier), 3); - const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier( - p, - factor, - ) as EvoTrackerModifier; - globalScene.addModifier(modifier); - } - }); - - return true; - } -} - -export class MoneyMultiplierModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof MoneyMultiplierModifier; - } - - clone(): MoneyMultiplierModifier { - return new MoneyMultiplierModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode MoneyMultiplierModifier} - * @param multiplier {@linkcode NumberHolder} holding the money multiplier value - * @returns always `true` - */ - override apply(multiplier: NumberHolder): boolean { - multiplier.value += Math.floor(multiplier.value * 0.2 * this.getStackCount()); - - return true; - } - - getMaxStackCount(): number { - return 5; - } -} - -export class DamageMoneyRewardModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof DamageMoneyRewardModifier; - } - - clone(): DamageMoneyRewardModifier { - return new DamageMoneyRewardModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode DamageMoneyRewardModifier} - * @param pokemon The {@linkcode Pokemon} attacking - * @param multiplier {@linkcode NumberHolder} holding the multiplier value - * @returns always `true` - */ - override apply(_pokemon: Pokemon, multiplier: NumberHolder): boolean { - const moneyAmount = new NumberHolder(Math.floor(multiplier.value * (0.5 * this.getStackCount()))); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); - globalScene.addMoney(moneyAmount.value); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 5; - } -} - -export class MoneyInterestModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof MoneyInterestModifier; - } - - /** - * Applies {@linkcode MoneyInterestModifier} - * @returns always `true` - */ - override apply(): boolean { - const interestAmount = Math.floor(globalScene.money * 0.1 * this.getStackCount()); - globalScene.addMoney(interestAmount); - - const userLocale = navigator.language || "en-US"; - const formattedMoneyAmount = interestAmount.toLocaleString(userLocale); - const message = i18next.t("modifier:moneyInterestApply", { - moneyAmount: formattedMoneyAmount, - typeName: this.type.name, - }); - globalScene.phaseManager.queueMessage(message, undefined, true); - - return true; - } - - clone(): MoneyInterestModifier { - return new MoneyInterestModifier(this.type, this.stackCount); - } - - getMaxStackCount(): number { - return 5; - } -} - -export class HiddenAbilityRateBoosterModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof HiddenAbilityRateBoosterModifier; - } - - clone(): HiddenAbilityRateBoosterModifier { - return new HiddenAbilityRateBoosterModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode HiddenAbilityRateBoosterModifier} - * @param boost {@linkcode NumberHolder} holding the boost value - * @returns always `true` - */ - override apply(boost: NumberHolder): boolean { - boost.value *= Math.pow(2, -1 - this.getStackCount()); - - return true; - } - - getMaxStackCount(): number { - return 4; - } -} - -export class ShinyRateBoosterModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof ShinyRateBoosterModifier; - } - - clone(): ShinyRateBoosterModifier { - return new ShinyRateBoosterModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode ShinyRateBoosterModifier} - * @param boost {@linkcode NumberHolder} holding the boost value - * @returns always `true` - */ - override apply(boost: NumberHolder): boolean { - boost.value *= Math.pow(2, 1 + this.getStackCount()); - - return true; - } - - getMaxStackCount(): number { - return 4; - } -} - -export class CriticalCatchChanceBoosterModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof CriticalCatchChanceBoosterModifier; - } - - clone(): CriticalCatchChanceBoosterModifier { - return new CriticalCatchChanceBoosterModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode CriticalCatchChanceBoosterModifier} - * @param boost {@linkcode NumberHolder} holding the boost value - * @returns always `true` - */ - override apply(boost: NumberHolder): boolean { - // 1 stack: 2x - // 2 stack: 2.5x - // 3 stack: 3x - boost.value *= 1.5 + this.getStackCount() / 2; - - return true; - } - - getMaxStackCount(): number { - return 3; - } -} - -export class LockModifierTiersModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof LockModifierTiersModifier; - } - - /** - * Applies {@linkcode LockModifierTiersModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - clone(): LockModifierTiersModifier { - return new LockModifierTiersModifier(this.type, this.stackCount); - } - - getMaxStackCount(): number { - return 1; - } -} - -/** - * Black Sludge item - */ -export class HealShopCostModifier extends PersistentModifier { - public readonly shopMultiplier: number; - - constructor(type: ModifierType, shopMultiplier: number, stackCount?: number) { - super(type, stackCount); - - this.shopMultiplier = shopMultiplier ?? 2.5; - } - - match(modifier: Modifier): boolean { - return modifier instanceof HealShopCostModifier; - } - - clone(): HealShopCostModifier { - return new HealShopCostModifier(this.type, this.shopMultiplier, this.stackCount); - } - - /** - * Applies {@linkcode HealShopCostModifier} - * @param cost {@linkcode NumberHolder} holding the heal shop cost - * @returns always `true` - */ - apply(moneyCost: NumberHolder): boolean { - moneyCost.value = Math.floor(moneyCost.value * this.shopMultiplier); - - return true; - } - - getArgs(): any[] { - return super.getArgs().concat(this.shopMultiplier); - } - - getMaxStackCount(): number { - return 1; - } -} - -export class BoostBugSpawnModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof BoostBugSpawnModifier; - } - - clone(): BoostBugSpawnModifier { - return new BoostBugSpawnModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode BoostBugSpawnModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export class SwitchEffectTransferModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof SwitchEffectTransferModifier; - } - - clone(): SwitchEffectTransferModifier { - return new SwitchEffectTransferModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode SwitchEffectTransferModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Abstract class for held items that steal other Pokemon's items. - * @see {@linkcode TurnHeldItemTransferModifier} - * @see {@linkcode ContactHeldItemTransferChanceModifier} - */ -export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { - /** - * Determines the targets to transfer items from when this applies. - * @param pokemon the {@linkcode Pokemon} holding this item - * @param _args N/A - * @returns the opponents of the source {@linkcode Pokemon} - */ - getTargets(pokemon?: Pokemon, ..._args: unknown[]): Pokemon[] { - return pokemon?.getOpponents?.() ?? []; - } - - /** - * Steals an item, chosen randomly, from a set of target Pokemon. - * @param pokemon The {@linkcode Pokemon} holding this item - * @param target The {@linkcode Pokemon} to steal from (optional) - * @param _args N/A - * @returns `true` if an item was stolen; false otherwise. - */ - override apply(pokemon: Pokemon, target?: Pokemon, ..._args: unknown[]): boolean { - const opponents = this.getTargets(pokemon, target); - - if (!opponents.length) { - return false; - } - - const targetPokemon = opponents[pokemon.randBattleSeedInt(opponents.length)]; - - const transferredItemCount = this.getTransferredItemCount(); - if (!transferredItemCount) { - return false; - } - - const transferredModifierTypes: ModifierType[] = []; - const itemModifiers = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === targetPokemon.id && m.isTransferable, - targetPokemon.isPlayer(), - ) as PokemonHeldItemModifier[]; - - for (let i = 0; i < transferredItemCount; i++) { - if (!itemModifiers.length) { - break; - } - const randItemIndex = pokemon.randBattleSeedInt(itemModifiers.length); - const randItem = itemModifiers[randItemIndex]; - if (globalScene.tryTransferHeldItemModifier(randItem, pokemon, false)) { - transferredModifierTypes.push(randItem.type); - itemModifiers.splice(randItemIndex, 1); + p.heldItemManager.add(HeldItemId.GIMMIGHOUL_EVO_TRACKER, factor); } } - for (const mt of transferredModifierTypes) { - globalScene.phaseManager.queueMessage(this.getTransferMessage(pokemon, targetPokemon, mt)); - } - - return !!transferredModifierTypes.length; - } - - abstract getTransferredItemCount(): number; - - abstract getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierType): string; -} - -/** - * Modifier for held items that steal items from the enemy at the end of - * each turn. - * @see {@linkcode modifierTypes[MINI_BLACK_HOLE]} - */ -export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { - isTransferable = true; - - matchType(modifier: Modifier): boolean { - return modifier instanceof TurnHeldItemTransferModifier; - } - - clone(): TurnHeldItemTransferModifier { - return new TurnHeldItemTransferModifier(this.type, this.pokemonId, this.stackCount); - } - - getTransferredItemCount(): number { - return this.getStackCount(); - } - - getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierType): string { - return i18next.t("modifier:turnHeldItemTransferApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), - itemName: item.name, - pokemonName: pokemon.getNameToRender(), - typeName: this.type.name, - }); - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } - - setTransferrableFalse(): void { - this.isTransferable = false; - } -} - -/** - * Modifier for held items that add a chance to steal items from the target of a - * successful attack. - * @see {@linkcode modifierTypes[GRIP_CLAW]} - * @see {@linkcode HeldItemTransferModifier} - */ -export class ContactHeldItemTransferChanceModifier extends HeldItemTransferModifier { - public readonly chance: number; - - constructor(type: ModifierType, pokemonId: number, chancePercent: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.chance = chancePercent / 100; - } - - /** - * Determines the target to steal items from when this applies. - * @param _holderPokemon The {@linkcode Pokemon} holding this item - * @param targetPokemon The {@linkcode Pokemon} the holder is targeting with an attack - * @returns The target {@linkcode Pokemon} as array for further use in `apply` implementations - */ - override getTargets(_holderPokemon: Pokemon, targetPokemon: Pokemon): Pokemon[] { - return targetPokemon ? [targetPokemon] : []; - } - - matchType(modifier: Modifier): boolean { - return modifier instanceof ContactHeldItemTransferChanceModifier; - } - - clone(): ContactHeldItemTransferChanceModifier { - return new ContactHeldItemTransferChanceModifier(this.type, this.pokemonId, this.chance * 100, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.chance * 100); - } - - getTransferredItemCount(): number { - return randSeedFloat() <= this.chance * this.getStackCount() ? 1 : 0; - } - - getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierType): string { - return i18next.t("modifier:contactHeldItemTransferApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), - itemName: item.name, - pokemonName: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }); - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 5; - } -} - -export class IvScannerModifier extends PersistentModifier { - constructor(type: ModifierType, _stackCount?: number) { - super(type); - } - - match(modifier: Modifier): boolean { - return modifier instanceof IvScannerModifier; - } - - clone(): IvScannerModifier { - return new IvScannerModifier(this.type); - } - - /** - * Applies {@linkcode IvScannerModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; //Dude are you kidding me - } - - getMaxStackCount(): number { - return 1; - } -} - -export class ExtraModifierModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof ExtraModifierModifier; - } - - clone(): ExtraModifierModifier { - return new ExtraModifierModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode ExtraModifierModifier} - * @param count {NumberHolder} holding the count value - * @returns always `true` - */ - override apply(count: NumberHolder): boolean { - count.value += this.getStackCount(); - return true; } - - getMaxStackCount(): number { - return 3; - } -} - -/** - * Modifier used for timed boosts to the player's shop item rewards. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} - */ -export class TempExtraModifierModifier extends LapsingPersistentModifier { - /** - * Goes through existing modifiers for any that match Silver Pokeball, - * which will then add the max count of the new item to the existing count of the current item. - * If no existing Silver Pokeballs are found, will add a new one. - * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers - * @param _virtual N/A - * @returns true if the modifier was successfully added or applied, false otherwise - */ - add(modifiers: PersistentModifier[], _virtual: boolean): boolean { - for (const modifier of modifiers) { - if (this.match(modifier)) { - const modifierInstance = modifier as TempExtraModifierModifier; - const newBattleCount = this.getMaxBattles() + modifierInstance.getBattleCount(); - - modifierInstance.setNewBattleCount(newBattleCount); - globalScene.playSound("se/restore"); - return true; - } - } - - modifiers.push(this); - return true; - } - - clone() { - return new TempExtraModifierModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount); - } - - match(modifier: Modifier): boolean { - return modifier instanceof TempExtraModifierModifier; - } - - /** - * Increases the current rewards in the battle by the `stackCount`. - * @returns `true` if the shop reward number modifier applies successfully - * @param count {@linkcode NumberHolder} that holds the resulting shop item reward count - */ - apply(count: NumberHolder): boolean { - count.value += this.getStackCount(); - return true; - } -} - -export abstract class EnemyPersistentModifier extends PersistentModifier { - getMaxStackCount(): number { - return 5; - } -} - -abstract class EnemyDamageMultiplierModifier extends EnemyPersistentModifier { - protected damageMultiplier: number; - - constructor(type: ModifierType, damageMultiplier: number, stackCount?: number) { - super(type, stackCount); - - this.damageMultiplier = damageMultiplier; - } - - /** - * Applies {@linkcode EnemyDamageMultiplierModifier} - * @param multiplier {NumberHolder} holding the multiplier value - * @returns always `true` - */ - override apply(multiplier: NumberHolder): boolean { - multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageMultiplier, this.getStackCount())); - - return true; - } - - getMaxStackCount(): number { - return 99; - } -} - -export class EnemyDamageBoosterModifier extends EnemyDamageMultiplierModifier { - constructor(type: ModifierType, _boostPercent: number, stackCount?: number) { - //super(type, 1 + ((boostPercent || 10) * 0.01), stackCount); - super(type, 1.05, stackCount); // Hardcode multiplier temporarily - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyDamageBoosterModifier; - } - - clone(): EnemyDamageBoosterModifier { - return new EnemyDamageBoosterModifier(this.type, (this.damageMultiplier - 1) * 100, this.stackCount); - } - - getArgs(): any[] { - return [(this.damageMultiplier - 1) * 100]; - } - - getMaxStackCount(): number { - return 999; - } -} - -export class EnemyDamageReducerModifier extends EnemyDamageMultiplierModifier { - constructor(type: ModifierType, _reductionPercent: number, stackCount?: number) { - //super(type, 1 - ((reductionPercent || 5) * 0.01), stackCount); - super(type, 0.975, stackCount); // Hardcode multiplier temporarily - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyDamageReducerModifier; - } - - clone(): EnemyDamageReducerModifier { - return new EnemyDamageReducerModifier(this.type, (1 - this.damageMultiplier) * 100, this.stackCount); - } - - getArgs(): any[] { - return [(1 - this.damageMultiplier) * 100]; - } - - getMaxStackCount(): number { - return globalScene.currentBattle.waveIndex < 2000 ? super.getMaxStackCount() : 999; - } -} - -export class EnemyTurnHealModifier extends EnemyPersistentModifier { - public healPercent: number; - - constructor(type: ModifierType, _healPercent: number, stackCount?: number) { - super(type, stackCount); - - // Hardcode temporarily - this.healPercent = 2; - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyTurnHealModifier; - } - - clone(): EnemyTurnHealModifier { - return new EnemyTurnHealModifier(this.type, this.healPercent, this.stackCount); - } - - getArgs(): any[] { - return [this.healPercent]; - } - - /** - * Applies {@linkcode EnemyTurnHealModifier} - * @param enemyPokemon The {@linkcode Pokemon} to heal - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(enemyPokemon: Pokemon): boolean { - if (!enemyPokemon.isFullHp()) { - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - enemyPokemon.getBattlerIndex(), - Math.max(Math.floor(enemyPokemon.getMaxHp() / (100 / this.healPercent)) * this.stackCount, 1), - i18next.t("modifier:enemyTurnHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), - }), - true, - false, - false, - false, - true, - ); - return true; - } - - return false; - } - - getMaxStackCount(): number { - return 10; - } -} - -export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifier { - public effect: StatusEffect; - public chance: number; - - constructor(type: ModifierType, effect: StatusEffect, _chancePercent: number, stackCount?: number) { - super(type, stackCount); - - this.effect = effect; - // Hardcode temporarily - this.chance = 0.025 * (this.effect === StatusEffect.BURN || this.effect === StatusEffect.POISON ? 2 : 1); - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyAttackStatusEffectChanceModifier && modifier.effect === this.effect; - } - - clone(): EnemyAttackStatusEffectChanceModifier { - return new EnemyAttackStatusEffectChanceModifier(this.type, this.effect, this.chance * 100, this.stackCount); - } - - getArgs(): any[] { - return [this.effect, this.chance * 100]; - } - - /** - * Applies {@linkcode EnemyAttackStatusEffectChanceModifier} - * @param enemyPokemon {@linkcode Pokemon} to apply the status effect to - * @returns `true` if the {@linkcode Pokemon} was affected - */ - override apply(enemyPokemon: Pokemon): boolean { - if (randSeedFloat() <= this.chance * this.getStackCount()) { - return enemyPokemon.trySetStatus(this.effect, true); - } - - return false; - } - - getMaxStackCount(): number { - return 10; - } -} - -export class EnemyStatusEffectHealChanceModifier extends EnemyPersistentModifier { - public chance: number; - - constructor(type: ModifierType, _chancePercent: number, stackCount?: number) { - super(type, stackCount); - - //Hardcode temporarily - this.chance = 0.025; - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyStatusEffectHealChanceModifier; - } - - clone(): EnemyStatusEffectHealChanceModifier { - return new EnemyStatusEffectHealChanceModifier(this.type, this.chance * 100, this.stackCount); - } - - getArgs(): any[] { - return [this.chance * 100]; - } - - /** - * Applies {@linkcode EnemyStatusEffectHealChanceModifier} to randomly heal status. - * @param enemyPokemon - The {@linkcode Pokemon} to heal - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(enemyPokemon: Pokemon): boolean { - if (!enemyPokemon.status || randSeedFloat() > this.chance * this.getStackCount()) { - return false; - } - - globalScene.phaseManager.queueMessage( - getStatusEffectHealText(enemyPokemon.status.effect, getPokemonNameWithAffix(enemyPokemon)), - ); - enemyPokemon.resetStatus(); - enemyPokemon.updateInfo(); - return true; - } - - getMaxStackCount(): number { - return 10; - } -} - -export class EnemyEndureChanceModifier extends EnemyPersistentModifier { - public chance: number; - - constructor(type: ModifierType, _chancePercent?: number, stackCount?: number) { - super(type, stackCount || 10); - - //Hardcode temporarily - this.chance = 2; - } - - match(modifier: Modifier) { - return modifier instanceof EnemyEndureChanceModifier; - } - - clone() { - return new EnemyEndureChanceModifier(this.type, this.chance, this.stackCount); - } - - getArgs(): any[] { - return [this.chance]; - } - - /** - * Applies a chance of enduring a lethal hit of an attack - * @param target the {@linkcode Pokemon} to apply the {@linkcode BattlerTagType.ENDURING} chance to - * @returns `true` if {@linkcode Pokemon} endured - */ - override apply(target: Pokemon): boolean { - if (target.waveData.endured || target.randBattleSeedInt(100) >= this.chance * this.getStackCount()) { - return false; - } - - target.addTag(BattlerTagType.ENDURE_TOKEN, 1); - - target.waveData.endured = true; - - return true; - } - - getMaxStackCount(): number { - return 10; - } -} - -export class EnemyFusionChanceModifier extends EnemyPersistentModifier { - private chance: number; - - constructor(type: ModifierType, chancePercent: number, stackCount?: number) { - super(type, stackCount); - - this.chance = chancePercent / 100; - } - - match(modifier: Modifier) { - return modifier instanceof EnemyFusionChanceModifier && modifier.chance === this.chance; - } - - clone() { - return new EnemyFusionChanceModifier(this.type, this.chance * 100, this.stackCount); - } - - getArgs(): any[] { - return [this.chance * 100]; - } - - /** - * Applies {@linkcode EnemyFusionChanceModifier} - * @param isFusion {@linkcode BooleanHolder} that will be set to `true` if the {@linkcode EnemyPokemon} is a fusion - * @returns `true` if the {@linkcode EnemyPokemon} is a fusion - */ - override apply(isFusion: BooleanHolder): boolean { - if (randSeedFloat() > this.chance * this.getStackCount()) { - return false; - } - - isFusion.value = true; - - return true; - } - - getMaxStackCount(): number { - return 10; - } } /** @@ -3765,39 +553,20 @@ export class EnemyFusionChanceModifier extends EnemyPersistentModifier { * - The enemy * @param isPlayer {@linkcode boolean} for whether the player (`true`) or enemy (`false`) is being overridden */ -export function overrideModifiers(isPlayer = true): void { - const modifiersOverride: ModifierOverride[] = isPlayer - ? Overrides.STARTING_MODIFIER_OVERRIDE - : Overrides.OPP_MODIFIER_OVERRIDE; - if (!modifiersOverride || modifiersOverride.length === 0 || !globalScene) { +export function overrideTrainerItems(isPlayer = true): void { + const trainerItemsOverride: TrainerItemConfiguration = isPlayer + ? Overrides.STARTING_TRAINER_ITEMS_OVERRIDE + : Overrides.OPP_TRAINER_ITEMS_OVERRIDE; + if (!trainerItemsOverride || trainerItemsOverride.length === 0 || !globalScene) { return; } // If it's the opponent, clear all of their current modifiers to avoid stacking if (!isPlayer) { - globalScene.clearEnemyModifiers(); + globalScene.clearEnemyItems(); } - for (const item of modifiersOverride) { - const modifierFunc = modifierTypes[item.name]; - let modifierType: ModifierType | null = modifierFunc(); - - if (modifierType.is("ModifierTypeGenerator")) { - const pregenArgs = "type" in item && item.type !== null ? [item.type] : undefined; - modifierType = modifierType.generateType([], pregenArgs); - } - - const modifier = modifierType && (modifierType.withIdFromFunc(modifierFunc).newModifier() as PersistentModifier); - if (modifier) { - modifier.stackCount = item.count || 1; - - if (isPlayer) { - globalScene.addModifier(modifier, true, false, false, true); - } else { - globalScene.addEnemyModifier(modifier, true, true); - } - } - } + globalScene.assignTrainerItemsFromConfiguration(trainerItemsOverride, isPlayer); } /** @@ -3808,7 +577,7 @@ export function overrideModifiers(isPlayer = true): void { * @param isPlayer {@linkcode boolean} for whether the {@linkcode pokemon} is the player's (`true`) or an enemy (`false`) */ export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void { - const heldItemsOverride: ModifierOverride[] = isPlayer + const heldItemsOverride: HeldItemConfiguration = isPlayer ? Overrides.STARTING_HELD_ITEMS_OVERRIDE : Overrides.OPP_HELD_ITEMS_OVERRIDE; if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) { @@ -3816,31 +585,10 @@ export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void { } if (!isPlayer) { - globalScene.clearEnemyHeldItemModifiers(pokemon); + pokemon.heldItemManager.clearItems(); } - for (const item of heldItemsOverride) { - const modifierFunc = modifierTypes[item.name]; - let modifierType: ModifierType | null = modifierFunc(); - const qty = item.count || 1; - - if (modifierType.is("ModifierTypeGenerator")) { - const pregenArgs = "type" in item && item.type !== null ? [item.type] : undefined; - modifierType = modifierType.generateType([], pregenArgs); - } - - const heldItemModifier = - modifierType && (modifierType.withIdFromFunc(modifierFunc).newModifier(pokemon) as PokemonHeldItemModifier); - if (heldItemModifier) { - heldItemModifier.pokemonId = pokemon.id; - heldItemModifier.stackCount = qty; - if (isPlayer) { - globalScene.addModifier(heldItemModifier, true, false, false, true); - } else { - globalScene.addEnemyModifier(heldItemModifier, true, true); - } - } - } + assignItemsFromConfiguration(heldItemsOverride, pokemon); } /** @@ -3851,42 +599,9 @@ export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void { * requiring modifier types to be imported in every file. */ const ModifierClassMap = Object.freeze({ - PersistentModifier, ConsumableModifier, AddPokeballModifier, AddVoucherModifier, - LapsingPersistentModifier, - DoubleBattleChanceBoosterModifier, - TempStatStageBoosterModifier, - TempCritBoosterModifier, - MapModifier, - MegaEvolutionAccessModifier, - GigantamaxAccessModifier, - TerastallizeAccessModifier, - PokemonHeldItemModifier, - LapsingPokemonHeldItemModifier, - BaseStatModifier, - EvoTrackerModifier, - PokemonBaseStatTotalModifier, - PokemonBaseStatFlatModifier, - PokemonIncrementingStatModifier, - StatBoosterModifier, - SpeciesStatBoosterModifier, - CritBoosterModifier, - SpeciesCritBoosterModifier, - AttackTypeBoosterModifier, - SurviveDamageModifier, - BypassSpeedChanceModifier, - FlinchChanceModifier, - TurnHealModifier, - TurnStatusEffectModifier, - HitHealModifier, - LevelIncrementBoosterModifier, - BerryModifier, - PreserveBerryModifier, - PokemonInstantReviveModifier, - ResetNegativeStatStageModifier, - FieldEffectModifier, ConsumablePokemonModifier, TerrastalizeModifier, PokemonHpRestoreModifier, @@ -3901,43 +616,6 @@ const ModifierClassMap = Object.freeze({ RememberMoveModifier, EvolutionItemModifier, FusePokemonModifier, - MultipleParticipantExpBonusModifier, - HealingBoosterModifier, - ExpBoosterModifier, - PokemonExpBoosterModifier, - ExpShareModifier, - ExpBalanceModifier, - PokemonFriendshipBoosterModifier, - PokemonNatureWeightModifier, - PokemonMoveAccuracyBoosterModifier, - PokemonMultiHitModifier, - PokemonFormChangeItemModifier, - MoneyRewardModifier, - DamageMoneyRewardModifier, - MoneyInterestModifier, - HiddenAbilityRateBoosterModifier, - ShinyRateBoosterModifier, - CriticalCatchChanceBoosterModifier, - LockModifierTiersModifier, - HealShopCostModifier, - BoostBugSpawnModifier, - SwitchEffectTransferModifier, - HeldItemTransferModifier, - TurnHeldItemTransferModifier, - ContactHeldItemTransferChanceModifier, - IvScannerModifier, - ExtraModifierModifier, - TempExtraModifierModifier, - EnemyPersistentModifier, - EnemyDamageMultiplierModifier, - EnemyDamageBoosterModifier, - EnemyDamageReducerModifier, - EnemyTurnHealModifier, - EnemyAttackStatusEffectChanceModifier, - EnemyStatusEffectHealChanceModifier, - EnemyEndureChanceModifier, - EnemyFusionChanceModifier, - MoneyMultiplierModifier, }); export type ModifierConstructorMap = typeof ModifierClassMap; diff --git a/src/overrides.ts b/src/overrides.ts index ea50e9b088c..fa8e342b161 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -23,6 +23,8 @@ import { TrainerType } from "#enums/trainer-type"; import { Unlockables } from "#enums/unlockables"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; +import { HeldItemConfiguration } from "./items/held-item-data-types"; +import { TrainerItemConfiguration } from "./items/trainer-item-data-types"; /** * This comment block exists to prevent IDEs from automatically removing unused imports @@ -271,18 +273,15 @@ class DefaultOverrides { * STARTING_HELD_ITEM_OVERRIDE = [{name: "BERRY"}] * ``` */ - readonly STARTING_MODIFIER_OVERRIDE: ModifierOverride[] = []; - /** - * Override array of {@linkcode ModifierOverride}s used to provide modifiers to enemies. - * - * Note that any previous modifiers are cleared. - */ - readonly OPP_MODIFIER_OVERRIDE: ModifierOverride[] = []; + /** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */ + readonly STARTING_TRAINER_ITEMS_OVERRIDE: TrainerItemConfiguration = []; + /** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */ + readonly OPP_TRAINER_ITEMS_OVERRIDE: TrainerItemConfiguration = []; /** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */ - readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; + readonly STARTING_HELD_ITEMS_OVERRIDE: HeldItemConfiguration = []; /** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */ - readonly OPP_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; + readonly OPP_HELD_ITEMS_OVERRIDE: HeldItemConfiguration = []; /** * Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave. diff --git a/src/phases/add-enemy-buff-modifier-phase.ts b/src/phases/add-enemy-buff-modifier-phase.ts index 218a3a653ff..fcbfe096d52 100644 --- a/src/phases/add-enemy-buff-modifier-phase.ts +++ b/src/phases/add-enemy-buff-modifier-phase.ts @@ -1,9 +1,7 @@ -import { ModifierTier } from "#enums/modifier-tier"; -import { regenerateModifierPoolThresholds, getEnemyBuffModifierForWave } from "#app/modifier/modifier-type"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { EnemyPersistentModifier } from "#app/modifier/modifier"; +import { RewardTier } from "#enums/reward-tier"; import { Phase } from "#app/phase"; import { globalScene } from "#app/global-scene"; +import { assignEnemyBuffTokenForWave } from "#app/items/trainer-item-pool"; export class AddEnemyBuffModifierPhase extends Phase { public readonly phaseName = "AddEnemyBuffModifierPhase"; @@ -11,26 +9,13 @@ export class AddEnemyBuffModifierPhase extends Phase { super.start(); const waveIndex = globalScene.currentBattle.waveIndex; - const tier = !(waveIndex % 1000) - ? ModifierTier.ULTRA - : !(waveIndex % 250) - ? ModifierTier.GREAT - : ModifierTier.COMMON; - - regenerateModifierPoolThresholds(globalScene.getEnemyParty(), ModifierPoolType.ENEMY_BUFF); + const tier = !(waveIndex % 1000) ? RewardTier.ULTRA : !(waveIndex % 250) ? RewardTier.GREAT : RewardTier.COMMON; const count = Math.ceil(waveIndex / 250); for (let i = 0; i < count; i++) { - globalScene.addEnemyModifier( - getEnemyBuffModifierForWave( - tier, - globalScene.findModifiers(m => m instanceof EnemyPersistentModifier, false), - ), - true, - true, - ); + assignEnemyBuffTokenForWave(tier); } - globalScene.updateModifiers(false, true); + globalScene.updateItems(false); this.end(); } } diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index f4e6725935a..cbef0224cae 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -12,7 +12,6 @@ import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import type { EnemyPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; import { achvs } from "#app/system/achv"; import type { PartyOption } from "#app/ui/party-ui-handler"; @@ -255,6 +254,7 @@ export class AttemptCapturePhase extends PokemonPhase { }), null, () => { + const heldItemConfig = pokemon.heldItemManager.generateHeldItemConfiguration(); const end = () => { globalScene.phaseManager.unshiftNew("VictoryPhase", this.battlerIndex); globalScene.pokemonInfoContainer.hide(); @@ -265,24 +265,20 @@ export class AttemptCapturePhase extends PokemonPhase { globalScene.addFaintedEnemyScore(pokemon); pokemon.hp = 0; pokemon.trySetStatus(StatusEffect.FAINT); - globalScene.clearEnemyHeldItemModifiers(); pokemon.leaveField(true, true, true); }; const addToParty = (slotIndex?: number) => { const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex); - const modifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) { globalScene.validateAchv(achvs.SHINY_PARTY); } - Promise.all(modifiers.map(m => globalScene.addModifier(m, true))).then(() => { - globalScene.updateModifiers(true); - removePokemon(); - if (newPokemon) { - newPokemon.loadAssets().then(end); - } else { - end(); - } - }); + globalScene.updateItems(true); + removePokemon(); + if (newPokemon) { + newPokemon.loadAssets().then(end); + } else { + end(); + } }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { @@ -307,6 +303,7 @@ export class AttemptCapturePhase extends PokemonPhase { pokemon.variant, pokemon.ivs, pokemon.nature, + heldItemConfig, pokemon, ); globalScene.ui.setMode( diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index 3709374287a..78d8d7c482d 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -40,8 +40,6 @@ export class AttemptRunPhase extends FieldPhase { onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy()), }); - globalScene.clearEnemyHeldItemModifiers(); - enemyField.forEach(enemyPokemon => { enemyPokemon.hideInfo().then(() => enemyPokemon.destroy()); enemyPokemon.hp = 0; diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 297e20cb445..900fd9eeb57 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -1,6 +1,5 @@ import { globalScene } from "#app/global-scene"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; -import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; export class BattleEndPhase extends BattlePhase { @@ -72,7 +71,6 @@ export class BattleEndPhase extends BattlePhase { globalScene.currentBattle.pickUpScatteredMoney(); } - globalScene.clearEnemyHeldItemModifiers(); for (const p of globalScene.getEnemyParty()) { try { p.destroy(); @@ -81,20 +79,9 @@ export class BattleEndPhase extends BattlePhase { } } - const lapsingModifiers = globalScene.findModifiers( - m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier, - ) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[]; - for (const m of lapsingModifiers) { - const args: any[] = []; - if (m instanceof LapsingPokemonHeldItemModifier) { - args.push(globalScene.getPokemonById(m.pokemonId)); - } - if (!m.lapse(...args)) { - globalScene.removeModifier(m); - } - } + globalScene.trainerItems.lapseItems(); - globalScene.updateModifiers(); + globalScene.updateItems(); this.end(); } } diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index 61124a7cda8..2b361427d7c 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -1,13 +1,14 @@ import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { CommonAnim } from "#enums/move-anims-common"; -import { BerryUsedEvent } from "#app/events/battle-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import { BerryModifier } from "#app/modifier/modifier"; import i18next from "i18next"; import { BooleanHolder } from "#app/utils/common"; import { FieldPhase } from "./field-phase"; import { globalScene } from "#app/global-scene"; import type Pokemon from "#app/field/pokemon"; +import { allHeldItems, applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { HeldItemCategoryId, isItemInCategory } from "#enums/held-item-id"; /** * The phase after attacks where the pokemon eat berries. @@ -31,10 +32,9 @@ export class BerryPhase extends FieldPhase { * @param pokemon - The {@linkcode Pokemon} to check */ eatBerries(pokemon: Pokemon): void { - const hasUsableBerry = !!globalScene.findModifier( - m => m instanceof BerryModifier && m.shouldApply(pokemon), - pokemon.isPlayer(), - ); + const hasUsableBerry = pokemon.getHeldItems().some(m => { + return isItemInCategory(m, HeldItemCategoryId.BERRY) && allHeldItems[m].shouldApply(pokemon); + }); if (!hasUsableBerry) { return; @@ -59,15 +59,8 @@ export class BerryPhase extends FieldPhase { CommonAnim.USE_ITEM, ); - for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) { - // No need to track berries being eaten; already done inside applyModifiers - if (berryModifier.consumed) { - berryModifier.consumed = false; - pokemon.loseHeldItem(berryModifier); - } - globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); - } - globalScene.updateModifiers(pokemon.isPlayer()); + applyHeldItems(HELD_ITEM_EFFECT.BERRY, { pokemon: pokemon }); + globalScene.updateItems(pokemon.isPlayer()); // AbilityId.CHEEK_POUCH only works once per round of nom noms applyAbAttrs("HealFromBerryUseAbAttr", { pokemon }); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 52c2b2e465d..ceb60a303b9 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -14,9 +14,6 @@ import { EncounterPhaseEvent } from "#app/events/battle-scene"; import type Pokemon from "#app/field/pokemon"; import { FieldPosition } from "#enums/field-position"; import { getPokemonNameWithAffix } from "#app/messages"; -import { BoostBugSpawnModifier, IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; -import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; import Overrides from "#app/overrides"; import { BattlePhase } from "#app/phases/battle-phase"; import { achvs } from "#app/system/achv"; @@ -28,10 +25,11 @@ import { BiomeId } from "#enums/biome-id"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { PlayerGender } from "#enums/player-gender"; import { SpeciesId } from "#enums/species-id"; -import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; +import { overrideHeldItems, overrideTrainerItems } from "#app/modifier/modifier"; import i18next from "i18next"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/constants"; import { getNatureName } from "#app/data/nature"; +import { TrainerItemId } from "#enums/trainer-item-id"; export class EncounterPhase extends BattlePhase { // Union type is necessary as this is subclassed, and typescript will otherwise complain @@ -107,7 +105,7 @@ export class EncounterPhase extends BattlePhase { let enemySpecies = globalScene.randomSpecies(battle.waveIndex, level, true); // If player has golden bug net, rolls 10% chance to replace non-boss wave wild species from the golden bug net bug pool if ( - globalScene.findModifier(m => m instanceof BoostBugSpawnModifier) && + globalScene.trainerItems.hasItem(TrainerItemId.GOLDEN_BUG_NET) && !globalScene.gameMode.isBoss(battle.waveIndex) && globalScene.arena.biomeType !== BiomeId.END && randSeedInt(10) === 0 @@ -271,12 +269,8 @@ export class EncounterPhase extends BattlePhase { if (!this.loaded && battle.battleType !== BattleType.MYSTERY_ENCOUNTER) { // generate modifiers for MEs, overriding prior ones as applicable - regenerateModifierPoolThresholds( - globalScene.getEnemyField(), - battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, - ); - globalScene.generateEnemyModifiers(); - overrideModifiers(false); + globalScene.generateEnemyItems(); + overrideTrainerItems(false); for (const enemy of globalScene.getEnemyField()) { overrideHeldItems(enemy, false); @@ -311,7 +305,7 @@ export class EncounterPhase extends BattlePhase { doEncounter() { globalScene.playBgm(undefined, true); - globalScene.updateModifiers(false); + globalScene.updateItems(false); globalScene.setFieldScale(1); const { battleType, waveIndex } = globalScene.currentBattle; @@ -349,6 +343,7 @@ export class EncounterPhase extends BattlePhase { } }, }); + globalScene.updateItems(false); const encounterIntroVisuals = globalScene.currentBattle?.mysteryEncounter?.introVisuals; if (encounterIntroVisuals) { @@ -543,22 +538,6 @@ export class EncounterPhase extends BattlePhase { if (enemyPokemon.isShiny(true)) { globalScene.phaseManager.unshiftNew("ShinySparklePhase", BattlerIndex.ENEMY + e); } - /** This sets Eternatus' held item to be untransferrable, preventing it from being stolen */ - if ( - enemyPokemon.species.speciesId === SpeciesId.ETERNATUS && - (globalScene.gameMode.isBattleClassicFinalBoss(globalScene.currentBattle.waveIndex) || - globalScene.gameMode.isEndlessMajorBoss(globalScene.currentBattle.waveIndex)) - ) { - const enemyMBH = globalScene.findModifier( - m => m instanceof TurnHeldItemTransferModifier, - false, - ) as TurnHeldItemTransferModifier; - if (enemyMBH) { - globalScene.removeModifier(enemyMBH, true); - enemyMBH.setTransferrableFalse(); - globalScene.addEnemyModifier(enemyMBH); - } - } }); if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { @@ -585,8 +564,7 @@ export class EncounterPhase extends BattlePhase { }, ), ); - const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); - if (ivScannerModifier) { + if (globalScene.trainerItems.hasItem(TrainerItemId.IV_SCANNER)) { enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); } } diff --git a/src/phases/exp-phase.ts b/src/phases/exp-phase.ts index 74768e86186..123d03de62b 100644 --- a/src/phases/exp-phase.ts +++ b/src/phases/exp-phase.ts @@ -1,9 +1,9 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import { ExpBoosterModifier } from "#app/modifier/modifier"; import i18next from "i18next"; import { NumberHolder } from "#app/utils/common"; import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; export class ExpPhase extends PlayerPartyMemberPokemonPhase { public readonly phaseName = "ExpPhase"; @@ -20,7 +20,7 @@ export class ExpPhase extends PlayerPartyMemberPokemonPhase { const pokemon = this.getPokemon(); const exp = new NumberHolder(this.expValue); - globalScene.applyModifiers(ExpBoosterModifier, true, exp); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.EXP_BOOSTER, { numberHolder: exp }); exp.value = Math.floor(exp.value); globalScene.ui.showText( i18next.t("battle:expGain", { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index c2658b62b23..9dc54ef4642 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -14,13 +14,14 @@ import { PokemonMove } from "#app/data/moves/pokemon-move"; import { HitResult } from "#enums/hit-result"; import type { PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { SwitchType } from "#enums/switch-type"; import i18next from "i18next"; import { PokemonPhase } from "./pokemon-phase"; import { isNullOrUndefined } from "#app/utils/common"; import { FRIENDSHIP_LOSS_FROM_FAINT } from "#app/data/balance/starters"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; export class FaintPhase extends PokemonPhase { public readonly phaseName = "FaintPhase"; @@ -54,17 +55,7 @@ export class FaintPhase extends PokemonPhase { faintPokemon.resetSummonData(); if (!this.preventInstantRevive) { - const instantReviveModifier = globalScene.applyModifier( - PokemonInstantReviveModifier, - this.player, - faintPokemon, - ) as PokemonInstantReviveModifier; - - if (instantReviveModifier) { - faintPokemon.loseHeldItem(instantReviveModifier); - globalScene.updateModifiers(this.player); - return this.end(); - } + applyHeldItems(HELD_ITEM_EFFECT.INSTANT_REVIVE, { pokemon: faintPokemon }); } /** diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 3f92f26b9b9..eaee1e7da23 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -18,7 +18,6 @@ import { PlayerGender } from "#enums/player-gender"; import { TrainerType } from "#enums/trainer-type"; import i18next from "i18next"; import type { SessionSaveData } from "#app/system/game-data"; -import PersistentModifierData from "#app/system/modifier-data"; import PokemonData from "#app/system/pokemon-data"; import ChallengeData from "#app/system/challenge-data"; import TrainerData from "#app/system/trainer-data"; @@ -285,12 +284,10 @@ export class GameOverPhase extends BattlePhase { gameMode: globalScene.gameMode.modeId, party: globalScene.getPlayerParty().map(p => new PokemonData(p)), enemyParty: globalScene.getEnemyParty().map(p => new PokemonData(p)), - modifiers: preWaveSessionData - ? preWaveSessionData.modifiers - : globalScene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), - enemyModifiers: preWaveSessionData - ? preWaveSessionData.enemyModifiers - : globalScene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), + trainerItems: preWaveSessionData ? preWaveSessionData.trainerItems : globalScene.trainerItems.generateSaveData(), + enemyTrainerItems: preWaveSessionData + ? preWaveSessionData.enemyTrainerItems + : globalScene.enemyTrainerItems.generateSaveData(), arena: new ArenaData(globalScene.arena), pokeballCounts: globalScene.pokeballCounts, money: Math.floor(globalScene.money), diff --git a/src/phases/modifier-reward-phase.ts b/src/phases/modifier-reward-phase.ts index 29bbe215fc0..18f0ba0ea17 100644 --- a/src/phases/modifier-reward-phase.ts +++ b/src/phases/modifier-reward-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import type { ModifierType } from "#app/modifier/modifier-type"; +import { TrainerItemReward, type ModifierType } from "#app/modifier/modifier-type"; import type { ModifierTypeFunc } from "#app/@types/modifier-types"; import { getModifierType } from "#app/utils/modifier-utils"; import i18next from "i18next"; @@ -26,12 +26,16 @@ export class ModifierRewardPhase extends BattlePhase { doReward(): Promise { return new Promise(resolve => { - const newModifier = this.modifierType.newModifier(); - globalScene.addModifier(newModifier); + if (this.modifierType instanceof TrainerItemReward) { + this.modifierType.apply(); + } else { + const newModifier = this.modifierType.newModifier(); + globalScene.addModifier(newModifier); + } globalScene.playSound("item_fanfare"); globalScene.ui.showText( i18next.t("battle:rewardGain", { - modifierName: newModifier?.type.name, + modifierName: this.modifierType.name, }), null, () => resolve(), diff --git a/src/phases/money-reward-phase.ts b/src/phases/money-reward-phase.ts index 52cb9ecb3ff..6fff53542ae 100644 --- a/src/phases/money-reward-phase.ts +++ b/src/phases/money-reward-phase.ts @@ -1,9 +1,9 @@ import { globalScene } from "#app/global-scene"; import { ArenaTagType } from "#app/enums/arena-tag-type"; -import { MoneyMultiplierModifier } from "#app/modifier/modifier"; import i18next from "i18next"; import { NumberHolder } from "#app/utils/common"; import { BattlePhase } from "./battle-phase"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; export class MoneyRewardPhase extends BattlePhase { public readonly phaseName = "MoneyRewardPhase"; @@ -18,7 +18,7 @@ export class MoneyRewardPhase extends BattlePhase { start() { const moneyAmount = new NumberHolder(globalScene.getWaveMoneyAmount(this.moneyMultiplier)); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) { moneyAmount.value *= 2; diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index fb421d7a25f..21f869110d5 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -28,15 +28,6 @@ import type Pokemon from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result"; import { HitResult } from "#enums/hit-result"; import { getPokemonNameWithAffix } from "#app/messages"; -import { - ContactHeldItemTransferChanceModifier, - DamageMoneyRewardModifier, - EnemyAttackStatusEffectChanceModifier, - EnemyEndureChanceModifier, - FlinchChanceModifier, - HitHealModifier, - PokemonMultiHitModifier, -} from "#app/modifier/modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#app/utils/common"; import type { nil } from "#app/utils/common"; @@ -49,7 +40,10 @@ import { HitCheckResult } from "#enums/hit-check-result"; import type Move from "#app/data/moves/move"; import { isFieldTargeted } from "#app/data/moves/move-utils"; import { DamageAchv } from "#app/system/achv"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; @@ -319,7 +313,7 @@ export class MoveEffectPhase extends PokemonPhase { // If Parental Bond is applicable, add another hit applyAbAttrs("AddSecondStrikeAbAttr", { pokemon: user, move, hitCount }); // If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses - globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount); + applyHeldItems(HELD_ITEM_EFFECT.MULTI_HIT, { pokemon: user, moveId: move.id, count: hitCount }); // Set the user's relevant turnData fields to reflect the final hit count user.turnData.hitCount = hitCount.value; user.turnData.hitsLeft = hitCount.value; @@ -419,7 +413,7 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); } - globalScene.applyModifiers(HitHealModifier, this.player, user); + applyHeldItems(HELD_ITEM_EFFECT.HIT_HEAL, { pokemon: user }); this.getTargets().forEach(target => { target.turnData.moveEffectiveness = null; }); @@ -461,7 +455,7 @@ export class MoveEffectPhase extends PokemonPhase { !this.move.hitsSubstitute(user, target) ) { const flinched = new BooleanHolder(false); - globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + applyHeldItems(HELD_ITEM_EFFECT.FLINCH_CHANCE, { pokemon: user, flinched: flinched }); if (flinched.value) { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.id, user.id); } @@ -862,7 +856,7 @@ export class MoveEffectPhase extends PokemonPhase { if (isBlockedBySubstitute) { substitute.hp -= dmg; } else if (!target.isPlayer() && dmg >= target.hp) { - globalScene.applyModifiers(EnemyEndureChanceModifier, false, target); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_ENDURE_CHANCE, { pokemon: target }); } const damage = isBlockedBySubstitute @@ -906,7 +900,7 @@ export class MoveEffectPhase extends PokemonPhase { }); if (user.isPlayer() && target.isEnemy()) { - globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage)); + applyHeldItems(HELD_ITEM_EFFECT.DAMAGE_MONEY_REWARD, { pokemon: user, damage: damage }); } return [result, isCritical]; @@ -1014,12 +1008,12 @@ export class MoveEffectPhase extends PokemonPhase { // We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens if (!user.isPlayer() && this.move.is("AttackMove")) { - globalScene.applyShuffledModifiers(EnemyAttackStatusEffectChanceModifier, false, target); + globalScene.applyShuffledStatusTokens(target); } // Apply Grip Claw's chance to steal an item from the target if (this.move.is("AttackMove")) { - globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); + applyHeldItems(HELD_ITEM_EFFECT.CONTACT_ITEM_STEAL_CHANCE, { pokemon: user, target: target }); } } } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 9aae796211f..1adb8f4d5ab 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -13,10 +13,10 @@ import { getCharVariantFromDialogue } from "../data/dialogue"; import type { OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; import { transitionMysteryEncounterIntroVisuals } from "../data/mystery-encounters/utils/encounter-phase-utils"; import { TrainerSlot } from "#enums/trainer-slot"; -import { IvScannerModifier } from "../modifier/modifier"; import { Phase } from "../phase"; import { UiMode } from "#enums/ui-mode"; import { isNullOrUndefined, randSeedItem } from "#app/utils/common"; +import { TrainerItemId } from "#enums/trainer-item-id"; /** * Will handle (in order): @@ -414,8 +414,7 @@ export class MysteryEncounterBattlePhase extends Phase { // PostSummon and ShinySparkle phases are handled by SummonPhase if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE) { - const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); - if (ivScannerModifier) { + if (globalScene.trainerItems.hasItem(TrainerItemId.IV_SCANNER)) { enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); } } diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index cf6cf40a923..1f758133d87 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -5,13 +5,13 @@ import { getStatusEffectHealText } from "#app/data/status-effect"; import { StatusEffect } from "#app/enums/status-effect"; import { HitResult } from "#enums/hit-result"; import { getPokemonNameWithAffix } from "#app/messages"; -import { HealingBoosterModifier } from "#app/modifier/modifier"; import { HealAchv } from "#app/system/achv"; import i18next from "i18next"; import { NumberHolder } from "#app/utils/common"; import { CommonAnimPhase } from "./common-anim-phase"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import type { HealBlockTag } from "#app/data/battler-tags"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; export class PokemonHealPhase extends CommonAnimPhase { public readonly phaseName = "PokemonHealPhase"; @@ -75,7 +75,7 @@ export class PokemonHealPhase extends CommonAnimPhase { if (healOrDamage) { const hpRestoreMultiplier = new NumberHolder(1); if (!this.revive) { - globalScene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HEALING_BOOSTER, { numberHolder: hpRestoreMultiplier }); } const healAmount = new NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value)); if (healAmount.value < 0) { diff --git a/src/phases/select-biome-phase.ts b/src/phases/select-biome-phase.ts index e8b4946b6d1..d87640d14e6 100644 --- a/src/phases/select-biome-phase.ts +++ b/src/phases/select-biome-phase.ts @@ -1,11 +1,11 @@ import { globalScene } from "#app/global-scene"; import { biomeLinks, getBiomeName } from "#app/data/balance/biomes"; import { BiomeId } from "#enums/biome-id"; -import { MoneyInterestModifier, MapModifier } from "#app/modifier/modifier"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { UiMode } from "#enums/ui-mode"; import { BattlePhase } from "./battle-phase"; import { randSeedInt } from "#app/utils/common"; +import { TrainerItemId } from "#enums/trainer-item-id"; export class SelectBiomePhase extends BattlePhase { public readonly phaseName = "SelectBiomePhase"; @@ -19,7 +19,6 @@ export class SelectBiomePhase extends BattlePhase { const setNextBiome = (nextBiome: BiomeId) => { if (nextWaveIndex % 10 === 1) { - globalScene.applyModifiers(MoneyInterestModifier, true); globalScene.phaseManager.unshiftNew("PartyHealPhase", false); } globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome); @@ -39,7 +38,7 @@ export class SelectBiomePhase extends BattlePhase { .filter(b => !Array.isArray(b) || !randSeedInt(b[1])) .map(b => (!Array.isArray(b) ? b : b[0])); - if (biomes.length > 1 && globalScene.findModifier(m => m instanceof MapModifier)) { + if (biomes.length > 1 && globalScene.trainerItems.hasItem(TrainerItemId.MAP)) { const biomeSelectItems = biomes.map(b => { const ret: OptionSelectItem = { label: getBiomeName(b), diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 53e1f5bc282..50b7594ca3e 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import type { ModifierTier } from "#enums/modifier-tier"; +import type { RewardTier } from "#enums/reward-tier"; import type { ModifierTypeOption, ModifierType } from "#app/modifier/modifier-type"; import { regenerateModifierPoolThresholds, @@ -12,15 +12,12 @@ import { PokemonPpRestoreModifierType, PokemonPpUpModifierType, getPlayerModifierTypeOptions, + HeldItemReward, + FormChangeItemReward, + TrainerItemReward, } from "#app/modifier/modifier-type"; import { ModifierPoolType } from "#enums/modifier-pool-type"; import type { Modifier } from "#app/modifier/modifier"; -import { - ExtraModifierModifier, - HealShopCostModifier, - PokemonHeldItemModifier, - TempExtraModifierModifier, -} from "#app/modifier/modifier"; import type ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; @@ -30,13 +27,14 @@ import { BattlePhase } from "./battle-phase"; import Overrides from "#app/overrides"; import type { CustomModifierSettings } from "#app/modifier/modifier-type"; import { isNullOrUndefined, NumberHolder } from "#app/utils/common"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; export type ModifierSelectCallback = (rowCursor: number, cursor: number) => boolean; export class SelectModifierPhase extends BattlePhase { public readonly phaseName = "SelectModifierPhase"; private rerollCount: number; - private modifierTiers?: ModifierTier[]; + private modifierTiers?: RewardTier[]; private customModifierSettings?: CustomModifierSettings; private isCopy: boolean; @@ -44,7 +42,7 @@ export class SelectModifierPhase extends BattlePhase { constructor( rerollCount = 0, - modifierTiers?: ModifierTier[], + modifierTiers?: RewardTier[], customModifierSettings?: CustomModifierSettings, isCopy = false, ) { @@ -154,7 +152,7 @@ export class SelectModifierPhase extends BattlePhase { const modifierType = shopOption.type; // Apply Black Sludge to healing item cost const healingItemCost = new NumberHolder(shopOption.cost); - globalScene.applyModifier(HealShopCostModifier, true, healingItemCost); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HEAL_SHOP_COST, { numberHolder: healingItemCost }); const cost = healingItemCost.value; if (globalScene.money < cost && !Overrides.WAIVE_ROLL_FEE_OVERRIDE) { @@ -172,11 +170,20 @@ export class SelectModifierPhase extends BattlePhase { modifierSelectCallback: ModifierSelectCallback, ): boolean { if (modifierType instanceof PokemonModifierType) { - if (modifierType instanceof FusePokemonModifierType) { + if (modifierType instanceof HeldItemReward || modifierType instanceof FormChangeItemReward) { + this.openGiveHeldItemMenu(modifierType, modifierSelectCallback); + } else if (modifierType instanceof FusePokemonModifierType) { this.openFusionMenu(modifierType, cost, modifierSelectCallback); } else { this.openModifierMenu(modifierType, cost, modifierSelectCallback); } + } else if (modifierType instanceof TrainerItemReward) { + console.log("WE GOT HERE"); + modifierType.apply(); + globalScene.updateItems(true); + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.MESSAGE); + super.end(); } else { this.applyModifier(modifierType.newModifier()!); } @@ -194,7 +201,7 @@ export class SelectModifierPhase extends BattlePhase { globalScene.phaseManager.unshiftNew( "SelectModifierPhase", this.rerollCount + 1, - this.typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[], + this.typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as RewardTier[], ); globalScene.ui.clearText(); globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end()); @@ -222,17 +229,15 @@ export class SelectModifierPhase extends BattlePhase { fromSlotIndex !== toSlotIndex && itemIndex > -1 ) { - const itemModifiers = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.isTransferable && m.pokemonId === party[fromSlotIndex].id, - ) as PokemonHeldItemModifier[]; - const itemModifier = itemModifiers[itemIndex]; - globalScene.tryTransferHeldItemModifier( - itemModifier, + const items = party[fromSlotIndex].heldItemManager.getTransferableHeldItems(); + const item = items[itemIndex]; + globalScene.tryTransferHeldItem( + item, + party[fromSlotIndex], party[toSlotIndex], true, itemQuantity, undefined, - undefined, false, ); } else { @@ -267,7 +272,7 @@ export class SelectModifierPhase extends BattlePhase { * @param playSound - Whether the 'obtain modifier' sound should be played when adding the modifier. */ private applyModifier(modifier: Modifier, cost = -1, playSound = false): void { - const result = globalScene.addModifier(modifier, false, playSound, undefined, undefined, cost); + const result = globalScene.addModifier(modifier, playSound, undefined, cost); // Queue a copy of this phase when applying a TM or Memory Mushroom. // If the player selects either of these, then escapes out of consuming them, // they are returned to a shop in the same state. @@ -372,11 +377,33 @@ export class SelectModifierPhase extends BattlePhase { ); } + private openGiveHeldItemMenu(reward, modifierSelectCallback) { + const party = globalScene.getPlayerParty(); + const partyUiMode = PartyUiMode.MODIFIER; + globalScene.ui.setModeWithoutClear( + UiMode.PARTY, + partyUiMode, + -1, + (slotIndex: number, _option: PartyOption) => { + if (slotIndex < 6) { + globalScene.ui.setMode(UiMode.MODIFIER_SELECT, this.isPlayer()).then(() => { + reward.apply(party[slotIndex]); + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.MESSAGE); + super.end(); + }); + } else { + this.resetModifierSelect(modifierSelectCallback); + } + }, + reward.selectFilter, + ); + } + // Function that determines how many reward slots are available private getModifierCount(): number { const modifierCountHolder = new NumberHolder(3); - globalScene.applyModifiers(ExtraModifierModifier, true, modifierCountHolder); - globalScene.applyModifiers(TempExtraModifierModifier, true, modifierCountHolder); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.EXTRA_REWARD, { numberHolder: modifierCountHolder }); // If custom modifiers are specified, overrides default item count if (this.customModifierSettings) { @@ -447,7 +474,7 @@ export class SelectModifierPhase extends BattlePhase { // Apply Black Sludge to reroll cost const modifiedRerollCost = new NumberHolder(baseMultiplier); - globalScene.applyModifier(HealShopCostModifier, true, modifiedRerollCost); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HEAL_SHOP_COST, { numberHolder: modifiedRerollCost }); return modifiedRerollCost.value; } diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index 76247c14ce0..ae77418261b 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -4,7 +4,7 @@ import { ChallengeType } from "#enums/challenge-type"; import { Gender } from "#app/data/gender"; import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; -import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; +import { overrideHeldItems, overrideTrainerItems } from "#app/modifier/modifier"; import Overrides from "#app/overrides"; import { Phase } from "#app/phase"; import { SaveSlotUiMode } from "#app/ui/save-slot-select-ui-handler"; @@ -102,7 +102,7 @@ export class SelectStarterPhase extends Phase { party.push(starterPokemon); loadPokemonAssets.push(starterPokemon.loadAssets()); }); - overrideModifiers(); + overrideTrainerItems(); overrideHeldItems(party[0]); Promise.all(loadPokemonAssets).then(() => { SoundFade.fadeOut(globalScene, globalScene.sound.get("menu"), 500, true); diff --git a/src/phases/show-party-exp-bar-phase.ts b/src/phases/show-party-exp-bar-phase.ts index 4849526b639..6c4e01fe840 100644 --- a/src/phases/show-party-exp-bar-phase.ts +++ b/src/phases/show-party-exp-bar-phase.ts @@ -1,9 +1,9 @@ import { globalScene } from "#app/global-scene"; import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; import { ExpNotification } from "#app/enums/exp-notification"; -import { ExpBoosterModifier } from "#app/modifier/modifier"; import { NumberHolder } from "#app/utils/common"; import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { public readonly phaseName = "ShowPartyExpBarPhase"; @@ -20,7 +20,7 @@ export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { const pokemon = this.getPokemon(); const exp = new NumberHolder(this.expValue); - globalScene.applyModifiers(ExpBoosterModifier, true, exp); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.EXP_BOOSTER, { numberHolder: exp }); exp.value = Math.floor(exp.value); const lastLevel = pokemon.level; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 77fb7b38600..0fd77648b76 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -6,7 +6,6 @@ import { ArenaTagSide } from "#enums/arena-tag-side"; import type { ArenaTag } from "#app/data/arena-tag"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; import { handleTutorial, Tutorial } from "#app/tutorial"; import { NumberHolder, BooleanHolder, isNullOrUndefined } from "#app/utils/common"; import i18next from "i18next"; @@ -14,6 +13,8 @@ import { PokemonPhase } from "./pokemon-phase"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; import { OctolockTag } from "#app/data/battler-tags"; import { ArenaTagType } from "#app/enums/arena-tag-type"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; import type { ConditionalUserFieldProtectStatAbAttrParams, PreStatStageChangeAbAttrParams, @@ -235,16 +236,7 @@ export class StatStageChangePhase extends PokemonPhase { ); if (!existingPhase?.is("StatStageChangePhase")) { // Apply White Herb if needed - const whiteHerb = globalScene.applyModifier( - ResetNegativeStatStageModifier, - this.player, - pokemon, - ) as ResetNegativeStatStageModifier; - // If the White Herb was applied, consume it - if (whiteHerb) { - pokemon.loseHeldItem(whiteHerb); - globalScene.updateModifiers(this.player); - } + applyHeldItems(HELD_ITEM_EFFECT.RESET_NEGATIVE_STAT_STAGE, { pokemon: pokemon, isPlayer: this.player }); } pokemon.updateInfo(); diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 95e4367d8df..a7dc1f18a6a 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -177,7 +177,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); } addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.getPokeball(true)); - globalScene.updateModifiers(this.player); + globalScene.updateItems(this.player); globalScene.updateFieldScale(); pokemon.showInfo(); pokemon.playAnim(); @@ -235,7 +235,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { } globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); } - globalScene.updateModifiers(this.player); + globalScene.updateItems(this.player); globalScene.updateFieldScale(); pokemon.showInfo(); pokemon.playAnim(); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index ccd0681c068..bff6311b744 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -6,12 +6,12 @@ import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-cha import { TrainerSlot } from "#enums/trainer-slot"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { SwitchEffectTransferModifier } from "#app/modifier/modifier"; import { Command } from "#enums/command"; import i18next from "i18next"; import { SummonPhase } from "./summon-phase"; import { SubstituteTag } from "#app/data/battler-tags"; import { SwitchType } from "#enums/switch-type"; +import { HeldItemId } from "#enums/held-item-id"; export class SwitchSummonPhase extends SummonPhase { public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" = "SwitchSummonPhase"; @@ -138,29 +138,11 @@ export class SwitchSummonPhase extends SummonPhase { ); // If the recipient pokemon lacks a baton, give our baton to it during the swap - if ( - !globalScene.findModifier( - m => - m instanceof SwitchEffectTransferModifier && - (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id, - ) - ) { - const batonPassModifier = globalScene.findModifier( - m => - m instanceof SwitchEffectTransferModifier && - (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id, - ) as SwitchEffectTransferModifier; + if (!switchedInPokemon.heldItemManager.hasItem(HeldItemId.BATON)) { + const batonPassModifier = this.lastPokemon.heldItemManager.hasItem(HeldItemId.BATON); if (batonPassModifier) { - globalScene.tryTransferHeldItemModifier( - batonPassModifier, - switchedInPokemon, - false, - undefined, - undefined, - undefined, - false, - ); + globalScene.tryTransferHeldItem(HeldItemId.BATON, this.lastPokemon, switchedInPokemon, false); } } } diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 5e36081b899..171a776bdb2 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -5,10 +5,6 @@ import { Gender } from "#app/data/gender"; import { getBiomeKey } from "#app/field/arena"; import { GameMode, getGameMode } from "#app/game-mode"; import { GameModes } from "#enums/game-modes"; -import type { Modifier } from "#app/modifier/modifier"; -import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; -import { modifierTypes } from "#app/data/data-lists"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; import { Phase } from "#app/phase"; import type { SessionSaveData } from "#app/system/game-data"; import { Unlockables } from "#enums/unlockables"; @@ -20,6 +16,8 @@ import { isLocal, isLocalServerConnected, isNullOrUndefined } from "#app/utils/c import i18next from "i18next"; import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; +import { assignDailyRunStarterHeldItems } from "#app/items/held-item-pool"; +import { TrainerItemId } from "#enums/trainer-item-id"; export class TitlePhase extends Phase { public readonly phaseName = "TitlePhase"; @@ -238,24 +236,13 @@ export class TitlePhase extends Phase { loadPokemonAssets.push(starterPokemon.loadAssets()); } - regenerateModifierPoolThresholds(party, ModifierPoolType.DAILY_STARTER); + globalScene.trainerItems.add(TrainerItemId.EXP_SHARE, 3); + globalScene.trainerItems.add(TrainerItemId.GOLDEN_EXP_CHARM, 3); + globalScene.trainerItems.add(TrainerItemId.MAP); - const modifiers: Modifier[] = Array(3) - .fill(null) - .map(() => modifierTypes.EXP_SHARE().withIdFromFunc(modifierTypes.EXP_SHARE).newModifier()) - .concat( - Array(3) - .fill(null) - .map(() => modifierTypes.GOLDEN_EXP_CHARM().withIdFromFunc(modifierTypes.GOLDEN_EXP_CHARM).newModifier()), - ) - .concat([modifierTypes.MAP().withIdFromFunc(modifierTypes.MAP).newModifier()]) - .concat(getDailyRunStarterModifiers(party)) - .filter(m => m !== null); + assignDailyRunStarterHeldItems(party); - for (const m of modifiers) { - globalScene.addModifier(m, true, false, false, true); - } - globalScene.updateModifiers(true, true); + globalScene.updateItems(true); Promise.all(loadPokemonAssets).then(() => { globalScene.time.delayedCall(500, () => globalScene.playBgm()); diff --git a/src/phases/trainer-item-reward-phase.ts b/src/phases/trainer-item-reward-phase.ts new file mode 100644 index 00000000000..29bbe215fc0 --- /dev/null +++ b/src/phases/trainer-item-reward-phase.ts @@ -0,0 +1,43 @@ +import { globalScene } from "#app/global-scene"; +import type { ModifierType } from "#app/modifier/modifier-type"; +import type { ModifierTypeFunc } from "#app/@types/modifier-types"; +import { getModifierType } from "#app/utils/modifier-utils"; +import i18next from "i18next"; +import { BattlePhase } from "./battle-phase"; + +export class ModifierRewardPhase extends BattlePhase { + // RibbonModifierRewardPhase extends ModifierRewardPhase and to make typescript happy + // we need to use a union type here + public readonly phaseName: "ModifierRewardPhase" | "RibbonModifierRewardPhase" | "GameOverModifierRewardPhase" = + "ModifierRewardPhase"; + protected modifierType: ModifierType; + + constructor(modifierTypeFunc: ModifierTypeFunc) { + super(); + + this.modifierType = getModifierType(modifierTypeFunc); + } + + start() { + super.start(); + + this.doReward().then(() => this.end()); + } + + doReward(): Promise { + return new Promise(resolve => { + const newModifier = this.modifierType.newModifier(); + globalScene.addModifier(newModifier); + globalScene.playSound("item_fanfare"); + globalScene.ui.showText( + i18next.t("battle:rewardGain", { + modifierName: newModifier?.type.name, + }), + null, + () => resolve(), + null, + true, + ); + }); + } +} diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index b5e56f6d63f..1d1bd3ebb3c 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -5,16 +5,12 @@ import { WeatherType } from "#app/enums/weather-type"; import { TurnEndEvent } from "#app/events/battle-scene"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { - TurnHealModifier, - EnemyTurnHealModifier, - EnemyStatusEffectHealChanceModifier, - TurnStatusEffectModifier, - TurnHeldItemTransferModifier, -} from "#app/modifier/modifier"; import i18next from "i18next"; import { FieldPhase } from "./field-phase"; import { globalScene } from "#app/global-scene"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; export class TurnEndPhase extends FieldPhase { public readonly phaseName = "TurnEndPhase"; @@ -30,7 +26,7 @@ export class TurnEndPhase extends FieldPhase { if (!pokemon.switchOutStatus) { pokemon.lapseTags(BattlerTagLapseType.TURN_END); - globalScene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); + applyHeldItems(HELD_ITEM_EFFECT.TURN_END_HEAL, { pokemon: pokemon }); if (globalScene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { globalScene.phaseManager.unshiftNew( @@ -45,15 +41,16 @@ export class TurnEndPhase extends FieldPhase { } if (!pokemon.isPlayer()) { - globalScene.applyModifiers(EnemyTurnHealModifier, false, pokemon); - globalScene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_HEAL, { pokemon: pokemon }); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_STATUS_HEAL_CHANCE, { pokemon: pokemon }); } applyAbAttrs("PostTurnAbAttr", { pokemon }); } - globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); - globalScene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); + applyHeldItems(HELD_ITEM_EFFECT.TURN_END_STATUS, { pokemon: pokemon }); + + applyHeldItems(HELD_ITEM_EFFECT.TURN_END_ITEM_STEAL, { pokemon: pokemon }); pokemon.tempSummonData.turnCount++; pokemon.tempSummonData.waveTurnCount++; diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index bfae2f06de4..cfdf68727f3 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -3,7 +3,6 @@ import { allMoves } from "#app/data/data-lists"; import { Stat } from "#app/enums/stat"; import type Pokemon from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; -import { BypassSpeedChanceModifier } from "#app/modifier/modifier"; import { Command } from "#enums/command"; import { randSeedShuffle, BooleanHolder } from "#app/utils/common"; import { FieldPhase } from "./field-phase"; @@ -11,6 +10,8 @@ import { BattlerIndex } from "#enums/battler-index"; import { TrickRoomTag } from "#app/data/arena-tag"; import { SwitchType } from "#enums/switch-type"; import { globalScene } from "#app/global-scene"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; +import { applyHeldItems } from "#app/items/all-held-items"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; @@ -72,7 +73,7 @@ export class TurnStartPhase extends FieldPhase { canCheckHeldItems: canCheckHeldItems, }); if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); + applyHeldItems(HELD_ITEM_EFFECT.BYPASS_SPEED_CHANCE, { pokemon: p, doBypassSpeed: bypassSpeed }); } battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; }); diff --git a/src/system/achv.ts b/src/system/achv.ts index 90816ff65c3..d94f05a6c9c 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -1,5 +1,4 @@ import type { Modifier } from "typescript"; -import { TurnHeldItemTransferModifier } from "../modifier/modifier"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import i18next from "i18next"; import { NumberHolder } from "#app/utils/common"; @@ -16,6 +15,8 @@ import type { ConditionFn } from "#app/@types/common"; import { Stat, getShortenedStatKey } from "#app/enums/stat"; import { Challenges } from "#app/enums/challenges"; import { globalScene } from "#app/global-scene"; +import { HeldItemId } from "#enums/held-item-id"; +import type Pokemon from "#app/field/pokemon"; export enum AchvTier { COMMON, @@ -189,6 +190,19 @@ export class ModifierAchv extends Achv { } } +export class HeldItemAchv extends Achv { + constructor( + localizationKey: string, + name: string, + description: string, + iconImage: string, + score: number, + pokemonFunc: (pokemon: Pokemon) => boolean, + ) { + super(localizationKey, name, description, iconImage, score, (args: any[]) => pokemonFunc(args[0] as Pokemon)); + } +} + export class ChallengeAchv extends Achv { constructor( localizationKey: string, @@ -490,13 +504,13 @@ export const achvs = { 25, ).setSecret(true), SPLICE: new Achv("SPLICE", "", "SPLICE.description", "dna_splicers", 10), - MINI_BLACK_HOLE: new ModifierAchv( + MINI_BLACK_HOLE: new HeldItemAchv( "MINI_BLACK_HOLE", "", "MINI_BLACK_HOLE.description", "mini_black_hole", 25, - modifier => modifier instanceof TurnHeldItemTransferModifier, + pokemon => pokemon.heldItemManager.hasItem(HeldItemId.MINI_BLACK_HOLE), ).setSecret(), CATCH_MYTHICAL: new Achv("CATCH_MYTHICAL", "", "CATCH_MYTHICAL.description", "strange_ball", 50).setSecret(), CATCH_SUB_LEGENDARY: new Achv("CATCH_SUB_LEGENDARY", "", "CATCH_SUB_LEGENDARY.description", "rb", 75).setSecret(), diff --git a/src/system/game-data.ts b/src/system/game-data.ts index d5d4256f7d0..f9cf5cfcdf0 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -12,7 +12,6 @@ import { speciesStarterCosts } from "#app/data/balance/starters"; import { randInt, getEnumKeys, isLocal, executeIf, fixedInt, randSeedItem, NumberHolder } from "#app/utils/common"; import Overrides from "#app/overrides"; import PokemonData from "#app/system/pokemon-data"; -import PersistentModifierData from "#app/system/modifier-data"; import ArenaData from "#app/system/arena-data"; import { Unlockables } from "#enums/unlockables"; import { getGameMode } from "#app/game-mode"; @@ -39,9 +38,6 @@ import { setSettingGamepad, SettingGamepad, settingGamepadDefaults } from "#app/ import type { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { setSettingKeyboard } from "#app/system/settings/settings-keyboard"; import { TagAddedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; -// biome-ignore lint/performance/noNamespaceImport: Something weird is going on here and I don't want to touch it -import * as Modifier from "#app/modifier/modifier"; -import { StatusEffect } from "#enums/status-effect"; import ChallengeData from "#app/system/challenge-data"; import { Device } from "#enums/devices"; import { GameDataType } from "#enums/game-data-type"; @@ -69,6 +65,7 @@ import { DexAttr } from "#enums/dex-attr"; import { AbilityAttr } from "#enums/ability-attr"; import { defaultStarterSpecies, saveKey } from "#app/constants"; import { encrypt, decrypt } from "#app/utils/data"; +import type { TrainerItemConfiguration, TrainerItemSaveData } from "#app/items/trainer-item-data-types"; function getDataTypeKey(dataType: GameDataType, slotId = 0): string { switch (dataType) { @@ -117,8 +114,8 @@ export interface SessionSaveData { gameMode: GameModes; party: PokemonData[]; enemyParty: PokemonData[]; - modifiers: PersistentModifierData[]; - enemyModifiers: PersistentModifierData[]; + trainerItems: TrainerItemSaveData; + enemyTrainerItems: TrainerItemSaveData; arena: ArenaData; pokeballCounts: PokeballCounts; money: number; @@ -917,8 +914,8 @@ export class GameData { gameMode: globalScene.gameMode.modeId, party: globalScene.getPlayerParty().map(p => new PokemonData(p)), enemyParty: globalScene.getEnemyParty().map(p => new PokemonData(p)), - modifiers: globalScene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), - enemyModifiers: globalScene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), + trainerItems: globalScene.trainerItems.generateSaveData(), + enemyTrainerItems: globalScene.enemyTrainerItems.generateSaveData(), arena: new ArenaData(globalScene.arena), pokeballCounts: globalScene.pokeballCounts, money: Math.floor(globalScene.money), @@ -946,6 +943,7 @@ export class GameData { } const handleSessionData = async (sessionDataStr: string) => { try { + console.log(sessionDataStr); const sessionData = this.parseSessionData(sessionDataStr); resolve(sessionData); } catch (err) { @@ -1097,28 +1095,16 @@ export class GameData { } } - if (globalScene.modifiers.length) { - console.warn("Existing modifiers not cleared on session load, deleting..."); - globalScene.modifiers = []; - } - for (const modifierData of sessionData.modifiers) { - const modifier = modifierData.toModifier(Modifier[modifierData.className]); - if (modifier) { - globalScene.addModifier(modifier, true); - } - } - globalScene.updateModifiers(true); + globalScene.trainerItems.clearItems(); + globalScene.assignTrainerItemsFromSaveData(sessionData.trainerItems, true); - for (const enemyModifierData of sessionData.enemyModifiers) { - const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); - if (modifier) { - globalScene.addEnemyModifier(modifier, true); - } - } - - globalScene.updateModifiers(false); + globalScene.enemyTrainerItems.clearItems(); + globalScene.assignTrainerItemsFromSaveData(sessionData.enemyTrainerItems, false); Promise.all(loadPokemonAssets).then(() => resolve(true)); + + globalScene.updateItems(true); + globalScene.updateItems(false); }; if (sessionData) { initSessionFromData(sessionData); @@ -1262,25 +1248,12 @@ export class GameData { case "trainer": return v ? new TrainerData(v) : null; - case "modifiers": - case "enemyModifiers": { - const ret: PersistentModifierData[] = []; + // TODO: Figure out what to do with this + case "trainerItems": + case "enemyTrainerItems": { + const ret: TrainerItemConfiguration = []; for (const md of v ?? []) { - if (md?.className === "ExpBalanceModifier") { - // Temporarily limit EXP Balance until it gets reworked - md.stackCount = Math.min(md.stackCount, 4); - } - - if ( - md instanceof Modifier.EnemyAttackStatusEffectChanceModifier && - (md.effect === StatusEffect.FREEZE || md.effect === StatusEffect.SLEEP) - ) { - // Discard any old "sleep/freeze chance tokens". - // TODO: make this migrate script - continue; - } - - ret.push(new PersistentModifierData(md, k === "modifiers")); + ret.push(md); } return ret; } @@ -1321,6 +1294,17 @@ export class GameData { if (sync) { globalScene.ui.savingIcon.show(); } + if (useCachedSession) { + console.log("REPARSING!"); + console.log( + decrypt( + localStorage.getItem( + `sessionData${globalScene.sessionSlotId ? globalScene.sessionSlotId : ""}_${loggedInUser?.username}`, + )!, + bypassLogin, + ), + ); + } const sessionData = useCachedSession ? this.parseSessionData( decrypt( @@ -1356,6 +1340,7 @@ export class GameData { `sessionData${globalScene.sessionSlotId ? globalScene.sessionSlotId : ""}_${loggedInUser?.username}`, encrypt(JSON.stringify(sessionData), bypassLogin), ); + console.log(JSON.stringify(sessionData)); console.debug("Session data saved!"); diff --git a/src/system/modifier-data.ts b/src/system/modifier-data.ts deleted file mode 100644 index cbda45572ac..00000000000 --- a/src/system/modifier-data.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import { PersistentModifier } from "#app/modifier/modifier"; -import type { GeneratedPersistentModifierType, ModifierType } from "#app/modifier/modifier-type"; -import { ModifierTypeGenerator, getModifierTypeFuncById } from "#app/modifier/modifier-type"; - -export default class ModifierData { - public player: boolean; - public typeId: string; - public typePregenArgs: any[]; - public args: any[]; - public stackCount: number; - - public className: string; - - constructor(source: PersistentModifier | any, player: boolean) { - const sourceModifier = source instanceof PersistentModifier ? (source as PersistentModifier) : null; - this.player = player; - this.typeId = sourceModifier ? sourceModifier.type.id : source.typeId; - if (sourceModifier) { - if ("getPregenArgs" in source.type) { - this.typePregenArgs = (source.type as GeneratedPersistentModifierType).getPregenArgs(); - } - } else if (source.typePregenArgs) { - this.typePregenArgs = source.typePregenArgs; - } - this.args = sourceModifier ? sourceModifier.getArgs() : source.args || []; - this.stackCount = source.stackCount; - this.className = sourceModifier ? sourceModifier.constructor.name : source.className; - } - - toModifier(_constructor: any): PersistentModifier | null { - const typeFunc = getModifierTypeFuncById(this.typeId); - if (!typeFunc) { - return null; - } - - try { - let type: ModifierType | null = typeFunc(); - type.id = this.typeId; - - if (type instanceof ModifierTypeGenerator) { - type = (type as ModifierTypeGenerator).generateType( - this.player ? globalScene.getPlayerParty() : globalScene.getEnemyField(), - this.typePregenArgs, - ); - } - - const ret = Reflect.construct( - _constructor, - ([type] as any[]).concat(this.args).concat(this.stackCount), - ) as PersistentModifier; - - if (ret.stackCount > ret.getMaxStackCount()) { - ret.stackCount = ret.getMaxStackCount(); - } - - return ret; - } catch (err) { - console.error(err); - return null; - } - } -} diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 28763fe970a..dacab225420 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -15,6 +15,8 @@ import type { MoveId } from "#enums/move-id"; import type { SpeciesId } from "#enums/species-id"; import { CustomPokemonData, PokemonBattleData, PokemonSummonData } from "#app/data/pokemon/pokemon-data"; import type { PokemonType } from "#enums/pokemon-type"; +import type { HeldItemSaveData } from "#app/items/held-item-data-types"; +import { saveDataToConfig } from "#app/items/held-item-pool"; export default class PokemonData { public id: number; @@ -35,6 +37,7 @@ export default class PokemonData { public stats: number[]; public ivs: number[]; public nature: Nature; + public heldItems: HeldItemSaveData; public moveset: PokemonMove[]; public status: Status | null; public friendship: number; @@ -102,6 +105,9 @@ export default class PokemonData { this.hp = source.hp; this.stats = source.stats; this.ivs = source.ivs; + console.log("SAVE ITEMS:", sourcePokemon?.heldItemManager.generateSaveData()); + console.log(sourcePokemon, sourcePokemon?.heldItemManager); + this.heldItems = sourcePokemon?.heldItemManager.generateSaveData() ?? source.heldItems; // TODO: Can't we move some of this verification stuff to an upgrade script? this.nature = source.nature ?? Nature.HARDY; @@ -154,6 +160,8 @@ export default class PokemonData { toPokemon(battleType?: BattleType, partyMemberIndex = 0, double = false): Pokemon { const species = getPokemonSpecies(this.species); + console.log("LOADED ITEMS:", this.heldItems); + console.log(saveDataToConfig(this.heldItems)); const ret: Pokemon = this.player ? globalScene.addPlayerPokemon( species, @@ -165,6 +173,7 @@ export default class PokemonData { this.variant, this.ivs, this.nature, + saveDataToConfig(this.heldItems), this, playerPokemon => { if (this.nickname) { @@ -182,6 +191,7 @@ export default class PokemonData { : TrainerSlot.NONE, this.boss, false, + saveDataToConfig(this.heldItems), this, ); diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index f73c6dab1b2..f1c9778db1d 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -10,6 +10,7 @@ import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER } from "./data/balance/starters"; import { MysteryEncounterType } from "./enums/mystery-encounter-type"; import { MysteryEncounterTier } from "./enums/mystery-encounter-tier"; import { Challenges } from "#enums/challenges"; +import { TrainerItemId } from "#enums/trainer-item-id"; export enum EventType { SHINY, @@ -59,7 +60,7 @@ interface TimedEvent extends EventBanner { startDate: Date; endDate: Date; eventEncounters?: EventEncounter[]; - delibirdyBuff?: string[]; + delibirdyBuff?: TrainerItemId[]; weather?: WeatherPoolEntry[]; mysteryEncounterTierChanges?: EventMysteryEncounterTier[]; luckBoostedSpecies?: SpeciesId[]; @@ -103,7 +104,14 @@ const timedEvents: TimedEvent[] = [ { species: SpeciesId.GALAR_DARUMAKA }, { species: SpeciesId.IRON_BUNDLE }, ], - delibirdyBuff: ["CATCHING_CHARM", "SHINY_CHARM", "ABILITY_CHARM", "EXP_CHARM", "SUPER_EXP_CHARM", "HEALING_CHARM"], + delibirdyBuff: [ + TrainerItemId.CATCHING_CHARM, + TrainerItemId.SHINY_CHARM, + TrainerItemId.ABILITY_CHARM, + TrainerItemId.EXP_CHARM, + TrainerItemId.SUPER_EXP_CHARM, + TrainerItemId.HEALING_CHARM, + ], weather: [{ weatherType: WeatherType.SNOW, weight: 1 }], mysteryEncounterTierChanges: [ { @@ -460,8 +468,8 @@ export class TimedEventManager { * For events where Delibirdy gives extra items * @returns list of ids of {@linkcode ModifierType}s that Delibirdy hands out as a bonus */ - getDelibirdyBuff(): string[] { - const ret: string[] = []; + getDelibirdyBuff(): TrainerItemId[] { + const ret: TrainerItemId[] = []; timedEvents .filter(te => this.isActive(te)) .map(te => { diff --git a/src/ui/battle-flyout.ts b/src/ui/battle-flyout.ts index 6f8f6a76b34..80f0f2601dd 100644 --- a/src/ui/battle-flyout.ts +++ b/src/ui/battle-flyout.ts @@ -177,8 +177,8 @@ export default class BattleFlyout extends Phaser.GameObjects.Container { const berryUsedEvent = event as BerryUsedEvent; if ( !berryUsedEvent || - berryUsedEvent.berryModifier.pokemonId !== this.pokemon?.id || - berryUsedEvent.berryModifier.berryType !== BerryType.LEPPA + berryUsedEvent.pokemon.id !== this.pokemon?.id || + berryUsedEvent.berryType !== BerryType.LEPPA ) { // We only care about Leppa berries return; diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 8df399b6d9b..417b663db36 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -7,11 +7,11 @@ import { Button } from "#enums/buttons"; import { getPokemonNameWithAffix } from "#app/messages"; import type { CommandPhase } from "#app/phases/command-phase"; import { globalScene } from "#app/global-scene"; -import { TerastallizeAccessModifier } from "#app/modifier/modifier"; import { PokemonType } from "#enums/pokemon-type"; import { getTypeRgb } from "#app/data/type"; import { SpeciesId } from "#enums/species-id"; import { Command } from "#enums/command"; +import { TrainerItemId } from "#enums/trainer-item-id"; export default class CommandUiHandler extends UiHandler { private commandsContainer: Phaser.GameObjects.Container; @@ -192,7 +192,7 @@ export default class CommandUiHandler extends UiHandler { } canTera(): boolean { - const hasTeraMod = !!globalScene.getModifiers(TerastallizeAccessModifier).length; + const hasTeraMod = !!globalScene.trainerItems.hasItem(TrainerItemId.TERA_ORB); const activePokemon = globalScene.getField()[this.fieldIndex]; const isBlockedForm = activePokemon.isMega() || activePokemon.isMax() || activePokemon.hasSpecies(SpeciesId.NECROZMA, "ultra"); diff --git a/src/ui/item-bar-ui.ts b/src/ui/item-bar-ui.ts new file mode 100644 index 00000000000..b18cd3a914c --- /dev/null +++ b/src/ui/item-bar-ui.ts @@ -0,0 +1,103 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { allHeldItems } from "#app/data/data-lists"; +import { allTrainerItems } from "#app/data/data-lists"; +import type { TrainerItemManager } from "#app/items/trainer-item-manager"; +import { heldItemSortFunc, trainerItemSortFunc } from "#app/items/item-utility"; + +const iconOverflowIndex = 24; + +export class ItemBar extends Phaser.GameObjects.Container { + private player: boolean; + private itemCache: number[]; + public totalVisibleLength = 0; + + constructor(enemy?: boolean) { + super(globalScene, 1 + (enemy ? 302 : 0), 2); + + this.player = !enemy; + this.setScale(0.5); + } + + /** + * Method to update content displayed in {@linkcode ItemBar} + * @param {PersistentItem[]} items - The list of items to be displayed in the {@linkcode ItemBar} + * @param {boolean} hideHeldItems - If set to "true", only items not assigned to a Pokémon are displayed + */ + updateItems(trainerItems: TrainerItemManager, pokemonA?: Pokemon, pokemonB?: Pokemon) { + this.removeAll(true); + + const sortedTrainerItems = trainerItems.getTrainerItems().sort(trainerItemSortFunc); + + const heldItemsA = pokemonA ? pokemonA.getHeldItems().sort(heldItemSortFunc) : []; + const heldItemsB = pokemonB ? pokemonB.getHeldItems().sort(heldItemSortFunc) : []; + + this.totalVisibleLength = sortedTrainerItems.length + heldItemsA.length + heldItemsB.length; + + let iconCount = 0; + sortedTrainerItems.forEach(item => { + const icon = allTrainerItems[item].createIcon(trainerItems.getStack(item)); + iconCount += 1; + this.addIcon(icon, iconCount, allTrainerItems[item].name, allTrainerItems[item].description); + }); + + if (pokemonA) { + heldItemsA.forEach(item => { + const icon = allHeldItems[item].createPokemonIcon(pokemonA); + iconCount += 1; + this.addIcon(icon, iconCount, allHeldItems[item].name, allHeldItems[item].description); + }); + } + + if (pokemonB) { + heldItemsB.forEach(item => { + const icon = allHeldItems[item].createPokemonIcon(pokemonB); + iconCount += 1; + this.addIcon(icon, iconCount, allHeldItems[item].name, allHeldItems[item].description); + }); + } + + for (const icon of this.getAll()) { + this.sendToBack(icon); + } + + this.itemCache = sortedTrainerItems.concat(heldItemsA).concat(heldItemsB); + } + + addIcon(icon: Phaser.GameObjects.Container, i: number, name: string, description: string) { + if (i >= iconOverflowIndex) { + icon.setVisible(false); + } + this.add(icon); + this.setItemIconPosition(icon, this.totalVisibleLength); + icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 24), Phaser.Geom.Rectangle.Contains); + icon.on("pointerover", () => { + globalScene.ui.showTooltip(name, description); + if (this.itemCache && this.itemCache.length > iconOverflowIndex) { + this.updateItemOverflowVisibility(true); + } + }); + icon.on("pointerout", () => { + globalScene.ui.hideTooltip(); + if (this.itemCache && this.itemCache.length > iconOverflowIndex) { + this.updateItemOverflowVisibility(false); + } + }); + } + + updateItemOverflowVisibility(ignoreLimit: boolean) { + const itemIcons = this.getAll().reverse(); + for (const item of itemIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) { + item.setVisible(ignoreLimit); + } + } + + setItemIconPosition(icon: Phaser.GameObjects.Container, itemCount: number) { + const rowIcons: number = 12 + 6 * Math.max(Math.ceil(Math.min(itemCount, 24) / 12) - 2, 0); + + const x = ((this.getIndex(icon) % rowIcons) * 26) / (rowIcons / 12); + const y = Math.floor(this.getIndex(icon) / rowIcons) * 20; + + icon.setPosition(this.player ? x : -x, y); + } +} diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 7f5bf997f88..8f3cddbdb52 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -5,7 +5,6 @@ import { getPokeballAtlasKey } from "#app/data/pokeball"; import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; import AwaitableUiHandler from "./awaitable-ui-handler"; import { UiMode } from "#enums/ui-mode"; -import { LockModifierTiersModifier, PokemonHeldItemModifier, HealShopCostModifier } from "../modifier/modifier"; import { handleTutorial, Tutorial } from "../tutorial"; import { Button } from "#enums/buttons"; import MoveInfoOverlay from "./move-info-overlay"; @@ -16,6 +15,8 @@ import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import Phaser from "phaser"; import type { PokeballType } from "#enums/pokeball"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item"; export const SHOP_OPTIONS_ROW_LIMIT = 7; const SINGLE_SHOP_ROW_YOFFSET = 12; @@ -183,8 +184,11 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.player = args[0]; const partyHasHeldItem = - this.player && !!globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable).length; - const canLockRarities = !!globalScene.findModifier(m => m instanceof LockModifierTiersModifier); + globalScene + .getPlayerParty() + .map(p => p.heldItemManager.getTransferableHeldItems().length) + .reduce((tot, i) => tot + i, 0) > 0; + const canLockRarities = !!globalScene.trainerItems.hasItem(TrainerItemId.LOCK_CAPSULE); this.transferButtonContainer.setVisible(false); this.transferButtonContainer.setAlpha(0); @@ -210,7 +214,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { const typeOptions = args[1] as ModifierTypeOption[]; const removeHealShop = globalScene.gameMode.hasNoShop; const baseShopCost = new NumberHolder(globalScene.getWaveMoneyAmount(1)); - globalScene.applyModifier(HealShopCostModifier, true, baseShopCost); + globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HEAL_SHOP_COST, { numberHolder: baseShopCost }); const shopTypeOptions = !removeHealShop ? getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value) : []; @@ -258,10 +262,12 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.shopOptionsRows[row].push(option); } - const maxUpgradeCount = typeOptions.map(to => to.upgradeCount).reduce((max, current) => Math.max(current, max), 0); + const maxUpgradeCount = typeOptions + .map(to => to.upgradeCount ?? 0) + .reduce((max, current) => Math.max(current, max), 0); - /* Force updateModifiers without pokemonSpecificModifiers */ - globalScene.getModifierBar().updateModifiers(globalScene.modifiers, true); + /* Force updateItems without pokemon held items */ + globalScene.updateItems(true, false); /* Multiplies the appearance duration by the speed parameter so that it is always constant, and avoids "flashbangs" at game speed x5 */ globalScene.showShopOverlay(750 * globalScene.gameSpeed); @@ -676,7 +682,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { globalScene.hideLuckText(250); /* Normally already called just after the shop, but not sure if it happens in 100% of cases */ - globalScene.getModifierBar().updateModifiers(globalScene.modifiers); + globalScene.updateItems(true); const options = this.options.concat(this.shopOptionsRows.flat()); this.options.splice(0, this.options.length); @@ -763,7 +769,7 @@ class ModifierOption extends Phaser.GameObjects.Container { this.add(this.itemContainer); const getItem = () => { - const item = globalScene.add.sprite(0, 0, "items", this.modifierTypeOption.type?.iconImage); + const item = globalScene.add.sprite(0, 0, "items", this.modifierTypeOption.type?.getIcon()); return item; }; diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 60e9e846859..fe0e039c6af 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -8,7 +8,6 @@ import { Command } from "#enums/command"; import MessageUiHandler from "#app/ui/message-ui-handler"; import { UiMode } from "#enums/ui-mode"; import { BooleanHolder, toReadableString, randInt, getLocalizedSpriteKey } from "#app/utils/common"; -import type { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { allMoves } from "#app/data/data-lists"; import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender"; import { StatusEffect } from "#enums/status-effect"; @@ -29,6 +28,9 @@ import { SpeciesId } from "#enums/species-id"; import { getPokemonNameWithAffix } from "#app/messages"; import type { CommandPhase } from "#app/phases/command-phase"; import { globalScene } from "#app/global-scene"; +import { HeldItemId } from "#enums/held-item-id"; +import { formChangeItemName } from "#app/data/pokemon-forms"; +import { allHeldItems } from "#app/data/data-lists"; const defaultMessage = i18next.t("partyUiHandler:choosePokemon"); @@ -139,10 +141,7 @@ export type PartyModifierTransferSelectCallback = ( ) => void; export type PartyModifierSpliceSelectCallback = (fromCursor: number, toCursor?: number) => void; export type PokemonSelectFilter = (pokemon: PlayerPokemon) => string | null; -export type PokemonModifierTransferSelectFilter = ( - pokemon: PlayerPokemon, - modifier: PokemonHeldItemModifier, -) => string | null; +export type PokemonModifierTransferSelectFilter = (pokemon: PlayerPokemon, item: HeldItemId) => string | null; export type PokemonMoveSelectFilter = (pokemonMove: PokemonMove) => string | null; export default class PartyUiHandler extends MessageUiHandler { @@ -221,11 +220,8 @@ export default class PartyUiHandler extends MessageUiHandler { private static FilterAllMoves = (_pokemonMove: PokemonMove) => null; - public static FilterItemMaxStacks = (pokemon: PlayerPokemon, modifier: PokemonHeldItemModifier) => { - const matchingModifier = globalScene.findModifier( - m => m.is("PokemonHeldItemModifier") && m.pokemonId === pokemon.id && m.matchType(modifier), - ) as PokemonHeldItemModifier; - if (matchingModifier && matchingModifier.stackCount === matchingModifier.getMaxStackCount()) { + public static FilterItemMaxStacks = (pokemon: PlayerPokemon, item: HeldItemId) => { + if (pokemon.heldItemManager.isMaxStack(item)) { return i18next.t("partyUiHandler:tooManyItems", { pokemonName: getPokemonNameWithAffix(pokemon, false) }); } return null; @@ -504,8 +500,10 @@ export default class PartyUiHandler extends MessageUiHandler { const ui = this.getUi(); if (this.transferCursor !== this.cursor) { if (this.transferAll) { - this.getTransferrableItemsFromPokemon(globalScene.getPlayerParty()[this.transferCursor]).forEach( - (_, i, array) => { + globalScene + .getPlayerParty() + [this.transferCursor].heldItemManager.getTransferableHeldItems() + .forEach((_, i, array) => { const invertedIndex = array.length - 1 - i; (this.selectCallback as PartyModifierTransferSelectCallback)( this.transferCursor, @@ -513,8 +511,7 @@ export default class PartyUiHandler extends MessageUiHandler { this.transferQuantitiesMax[invertedIndex], this.cursor, ); - }, - ); + }); } else { (this.selectCallback as PartyModifierTransferSelectCallback)( this.transferCursor, @@ -549,18 +546,15 @@ export default class PartyUiHandler extends MessageUiHandler { const newPokemon = globalScene.getPlayerParty()[p]; // this next bit checks to see if the the selected item from the original transfer pokemon exists on the new pokemon `p` // this returns `undefined` if the new pokemon doesn't have the item at all, otherwise it returns the `pokemonHeldItemModifier` for that item - const matchingModifier = globalScene.findModifier( - m => - m.is("PokemonHeldItemModifier") && - m.pokemonId === newPokemon.id && - m.matchType(this.getTransferrableItemsFromPokemon(pokemon)[this.transferOptionCursor]), - ) as PokemonHeldItemModifier; + const transferItem = pokemon.heldItemManager.getTransferableHeldItems()[this.transferOptionCursor]; + const matchingItem = newPokemon.heldItemManager.hasItem(transferItem); + const partySlot = this.partySlots.filter(m => m.getPokemon() === newPokemon)[0]; // this gets pokemon [p] for us if (p !== this.transferCursor) { // this skips adding the able/not able labels on the pokemon doing the transfer - if (matchingModifier) { + if (matchingItem) { // if matchingModifier exists then the item exists on the new pokemon - if (matchingModifier.getMaxStackCount() === matchingModifier.stackCount) { + if (newPokemon.heldItemManager.isMaxStack(transferItem)) { // checks to see if the stack of items is at max stack; if so, set the description label to "Not able" ableToTransferText = i18next.t("partyUiHandler:notAble"); } else { @@ -682,12 +676,6 @@ export default class PartyUiHandler extends MessageUiHandler { return success; } - private getTransferrableItemsFromPokemon(pokemon: PlayerPokemon) { - return globalScene.findModifiers( - m => m.is("PokemonHeldItemModifier") && m.isTransferable && m.pokemonId === pokemon.id, - ) as PokemonHeldItemModifier[]; - } - private getFilterResult(option: number, pokemon: PlayerPokemon): string | null { let filterResult: string | null; if (option !== PartyOption.TRANSFER && option !== PartyOption.SPLICE) { @@ -701,7 +689,7 @@ export default class PartyUiHandler extends MessageUiHandler { } else { filterResult = (this.selectFilter as PokemonModifierTransferSelectFilter)( pokemon, - this.getTransferrableItemsFromPokemon(globalScene.getPlayerParty()[this.transferCursor])[ + globalScene.getPlayerParty()[this.transferCursor].heldItemManager.getTransferableHeldItems()[ this.transferOptionCursor ], ); @@ -752,9 +740,9 @@ export default class PartyUiHandler extends MessageUiHandler { globalScene.phaseManager.getCurrentPhase()?.is("SelectModifierPhase") && this.partyUiMode === PartyUiMode.CHECK ) { - const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon); - const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; - modifier.active = !modifier.active; + const formChangeItems = this.getFormChangeItems(pokemon); + const item = formChangeItems[option - PartyOption.FORM_CHANGE_ITEM]; + pokemon.heldItemManager.toggleActive(item); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger, false, true); } @@ -923,14 +911,10 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.cursor < 6) { if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) { /** Initialize item quantities for the selected Pokemon */ - const itemModifiers = globalScene.findModifiers( - m => - m.is("PokemonHeldItemModifier") && - m.isTransferable && - m.pokemonId === globalScene.getPlayerParty()[this.cursor].id, - ) as PokemonHeldItemModifier[]; - this.transferQuantities = itemModifiers.map(item => item.getStackCount()); - this.transferQuantitiesMax = itemModifiers.map(item => item.getStackCount()); + const pokemon = globalScene.getPlayerParty()[this.cursor]; + const items = pokemon.heldItemManager.getTransferableHeldItems(); + this.transferQuantities = items.map(item => pokemon.heldItemManager.getStack(item)); + this.transferQuantitiesMax = items.map(item => pokemon.heldItemManager.getStack(item)); } this.showOptions(); ui.playSelect(); @@ -1161,9 +1145,7 @@ export default class PartyUiHandler extends MessageUiHandler { private allowBatonModifierSwitch(): boolean { return !!( this.partyUiMode !== PartyUiMode.FAINT_SWITCH && - globalScene.findModifier( - m => m.is("SwitchEffectTransferModifier") && m.pokemonId === globalScene.getPlayerField()[this.fieldIndex].id, - ) + globalScene.getPlayerField()[this.fieldIndex].heldItemManager.hasItem(HeldItemId.BATON) ); } @@ -1178,14 +1160,6 @@ export default class PartyUiHandler extends MessageUiHandler { ); } - private getItemModifiers(pokemon: Pokemon): PokemonHeldItemModifier[] { - return ( - (globalScene.findModifiers( - m => m.is("PokemonHeldItemModifier") && m.isTransferable && m.pokemonId === pokemon.id, - ) as PokemonHeldItemModifier[]) ?? [] - ); - } - private updateOptionsWithRememberMoveModifierMode(pokemon): void { const learnableMoves = pokemon.getLearnableLevelMoves(); for (let m = 0; m < learnableMoves.length; m++) { @@ -1205,11 +1179,11 @@ export default class PartyUiHandler extends MessageUiHandler { } private updateOptionsWithModifierTransferMode(pokemon): void { - const itemModifiers = this.getItemModifiers(pokemon); - for (let im = 0; im < itemModifiers.length; im++) { + const items = pokemon.getHeldItems(); + for (let im = 0; im < items.length; im++) { this.options.push(im); } - if (itemModifiers.length > 1) { + if (items.length > 1) { this.options.push(PartyOption.ALL); } } @@ -1336,8 +1310,8 @@ export default class PartyUiHandler extends MessageUiHandler { break; case PartyUiMode.CHECK: if (globalScene.phaseManager.getCurrentPhase()?.is("SelectModifierPhase")) { - const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon); - for (let i = 0; i < formChangeItemModifiers.length; i++) { + const formChangeItems = this.getFormChangeItems(pokemon); + for (let i = 0; i < formChangeItems.length; i++) { this.options.push(PartyOption.FORM_CHANGE_ITEM + i); } } @@ -1398,10 +1372,10 @@ export default class PartyUiHandler extends MessageUiHandler { break; } default: { - const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon); - if (formChangeItemModifiers && option >= PartyOption.FORM_CHANGE_ITEM) { - const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; - optionName = `${modifier.active ? i18next.t("partyUiHandler:DEACTIVATE") : i18next.t("partyUiHandler:ACTIVATE")} ${modifier.type.name}`; + const formChangeItems = this.getFormChangeItems(pokemon); + if (formChangeItems && option >= PartyOption.FORM_CHANGE_ITEM) { + const item = formChangeItems[option - PartyOption.FORM_CHANGE_ITEM]; + optionName = `${pokemon.heldItemManager.hasActiveFormChangeItem(item) ? i18next.t("partyUiHandler:DEACTIVATE") : i18next.t("partyUiHandler:ACTIVATE")} ${formChangeItemName(item)}`; } else if (option === PartyOption.UNPAUSE_EVOLUTION) { optionName = `${pokemon.pauseEvolutions ? i18next.t("partyUiHandler:UNPAUSE_EVOLUTION") : i18next.t("partyUiHandler:PAUSE_EVOLUTION")}`; } else { @@ -1425,9 +1399,9 @@ export default class PartyUiHandler extends MessageUiHandler { } else if (option === PartyOption.ALL) { optionName = i18next.t("partyUiHandler:ALL"); } else { - const itemModifiers = this.getItemModifiers(pokemon); - const itemModifier = itemModifiers[option]; - optionName = itemModifier.type.name; + const items = pokemon.getHeldItems(); + const item = items[option]; + optionName = allHeldItems[item].name; } const yCoord = -6 - 16 * o; @@ -1439,19 +1413,19 @@ export default class PartyUiHandler extends MessageUiHandler { optionText.setOrigin(0, 0); /** For every item that has stack bigger than 1, display the current quantity selection */ - const itemModifiers = this.getItemModifiers(pokemon); - const itemModifier = itemModifiers[option]; + const items = pokemon.getHeldItems(); + const item = items[option]; if ( this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && this.transferQuantitiesMax[option] > 1 && !this.transferMode && - itemModifier !== undefined && - itemModifier.type.name === optionName + item !== undefined && + allHeldItems[item].name === optionName ) { let amountText = ` (${this.transferQuantities[option]})`; /** If the amount held is the maximum, display the count in red */ - if (this.transferQuantitiesMax[option] === itemModifier.getMaxHeldItemCount(undefined)) { + if (this.transferQuantitiesMax[option] === allHeldItems[item].maxStackCount) { amountText = `[color=${getTextColor(TextStyle.SUMMARY_RED)}]${amountText}[/color]`; } @@ -1500,7 +1474,6 @@ export default class PartyUiHandler extends MessageUiHandler { null, () => { this.clearPartySlots(); - globalScene.removePartyMemberModifiers(slotIndex); const releasedPokemon = globalScene.getPlayerParty().splice(slotIndex, 1)[0]; releasedPokemon.destroy(); this.populatePartySlots(); @@ -1561,29 +1534,24 @@ export default class PartyUiHandler extends MessageUiHandler { }); } - getFormChangeItemsModifiers(pokemon: Pokemon) { - let formChangeItemModifiers = globalScene.findModifiers( - m => m.is("PokemonFormChangeItemModifier") && m.pokemonId === pokemon.id, - ) as PokemonFormChangeItemModifier[]; - const ultraNecrozmaModifiers = formChangeItemModifiers.filter( - m => m.active && m.formChangeItem === FormChangeItem.ULTRANECROZIUM_Z, - ); - if (ultraNecrozmaModifiers.length > 0) { + getFormChangeItems(pokemon: Pokemon) { + let formChangeItems = pokemon.heldItemManager.getFormChangeItems(); + const hasActiveFormChangeItems = pokemon.heldItemManager.getFormChangeItems().length; + const ultraNecrozmaActive = pokemon.heldItemManager.hasActiveFormChangeItem(FormChangeItem.ULTRANECROZIUM_Z); + if (ultraNecrozmaActive) { // ULTRANECROZIUM_Z is active and deactivating it should be the only option - return ultraNecrozmaModifiers; + return [FormChangeItem.ULTRANECROZIUM_Z]; } - if (formChangeItemModifiers.find(m => m.active)) { + if (hasActiveFormChangeItems) { // a form is currently active. the user has to disable the form or activate ULTRANECROZIUM_Z - formChangeItemModifiers = formChangeItemModifiers.filter( - m => m.active || m.formChangeItem === FormChangeItem.ULTRANECROZIUM_Z, + formChangeItems = formChangeItems.filter( + m => pokemon.heldItemManager.hasActiveFormChangeItem(m) || m === FormChangeItem.ULTRANECROZIUM_Z, ); } else if (pokemon.species.speciesId === SpeciesId.NECROZMA) { // no form is currently active. the user has to activate some form, except ULTRANECROZIUM_Z - formChangeItemModifiers = formChangeItemModifiers.filter( - m => m.formChangeItem !== FormChangeItem.ULTRANECROZIUM_Z, - ); + formChangeItems = formChangeItems.filter(m => m !== FormChangeItem.ULTRANECROZIUM_Z); } - return formChangeItemModifiers; + return formChangeItems; } getOptionsCursorWithScroll(): number { diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 76e343d018a..4d4f71dc7ed 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -19,14 +19,15 @@ import { PokemonType } from "#enums/pokemon-type"; import { TypeColor, TypeShadow } from "#app/enums/color"; import { getNatureStatMultiplier, getNatureName } from "../data/nature"; import { getVariantTint } from "#app/sprites/variant"; -// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` -import * as Modifier from "#app/modifier/modifier"; import type { SpeciesId } from "#enums/species-id"; import { PlayerGender } from "#enums/player-gender"; import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { getBiomeName } from "#app/data/balance/biomes"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; +import { allTrainerItems } from "#app/data/data-lists"; +import { allHeldItems } from "#app/data/data-lists"; +import { heldItemSortFunc } from "#app/items/item-utility"; /** * RunInfoUiMode indicates possible overlays of RunInfoUiHandler. @@ -66,7 +67,6 @@ export default class RunInfoUiHandler extends UiHandler { private endCardContainer: Phaser.GameObjects.Container; private partyVisibility: boolean; - private modifiersModule: any; constructor() { super(UiMode.RUN_INFO); @@ -74,8 +74,6 @@ export default class RunInfoUiHandler extends UiHandler { override async setup() { this.runContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1); - // The import of the modifiersModule is loaded here to sidestep async/await issues. - this.modifiersModule = Modifier; this.runContainer.setVisible(false); globalScene.loadImage("encounter_exclaim", "mystery-encounters"); } @@ -176,7 +174,7 @@ export default class RunInfoUiHandler extends UiHandler { const headerBg = addWindow(0, 0, globalScene.game.canvas.width / 6 - 2, 24); headerBg.setOrigin(0, 0); this.runContainer.add(headerBg); - if (this.runInfo.modifiers.length !== 0) { + if (this.runInfo.trainerItems.length !== 0) { const headerBgCoords = headerBg.getTopRight(); const abilityButtonContainer = globalScene.add.container(0, 0); const abilityButtonText = addTextObject(8, 0, i18next.t("runHistory:viewHeldItems"), TextStyle.WINDOW, { @@ -641,34 +639,31 @@ export default class RunInfoUiHandler extends UiHandler { // Player Held Items // A max of 20 items can be displayed. A + sign will be added if the run's held items pushes past this maximum to show the user that there are more. - if (this.runInfo.modifiers.length) { - let visibleModifierIndex = 0; + if (this.runInfo.trainerItems.length) { + let visibleTrainerItemIndex = 0; - const modifierIconsContainer = globalScene.add.container( + const trainerItemIconsContainer = globalScene.add.container( 8, this.runInfo.gameMode === GameModes.CHALLENGE ? 20 : 15, ); - modifierIconsContainer.setScale(0.45); - for (const m of this.runInfo.modifiers) { - const modifier = m.toModifier(this.modifiersModule[m.className]); - if (modifier instanceof Modifier.PokemonHeldItemModifier) { - continue; - } - const icon = modifier?.getIcon(false); + trainerItemIconsContainer.setScale(0.45); + for (const m of this.runInfo.trainerItems) { + const itemId = m.id; + const icon = allTrainerItems[itemId].createIcon(m.stack); if (icon) { - const rowHeightModifier = Math.floor(visibleModifierIndex / 7); - icon.setPosition(24 * (visibleModifierIndex % 7), 20 + 35 * rowHeightModifier); - modifierIconsContainer.add(icon); + const rowHeightTrainerItem = Math.floor(visibleTrainerItemIndex / 7); + icon.setPosition(24 * (visibleTrainerItemIndex % 7), 20 + 35 * rowHeightTrainerItem); + trainerItemIconsContainer.add(icon); } - if (++visibleModifierIndex === 20) { + if (++visibleTrainerItemIndex === 20) { const maxItems = addTextObject(45, 90, "+", TextStyle.WINDOW); - maxItems.setPositionRelative(modifierIconsContainer, 70, 45); + maxItems.setPositionRelative(trainerItemIconsContainer, 70, 45); this.runInfoContainer.add(maxItems); break; } } - this.runInfoContainer.add(modifierIconsContainer); + this.runInfoContainer.add(trainerItemIconsContainer); } this.runInfoContainer.add(modeText); @@ -884,38 +879,22 @@ export default class RunInfoUiHandler extends UiHandler { const heldItemsScale = this.runInfo.gameMode === GameModes.SPLICED_ENDLESS || this.runInfo.gameMode === GameModes.ENDLESS ? 0.25 : 0.5; const heldItemsContainer = globalScene.add.container(-82, 2); - const heldItemsList: Modifier.PokemonHeldItemModifier[] = []; - if (this.runInfo.modifiers.length) { - for (const m of this.runInfo.modifiers) { - const modifier = m.toModifier(this.modifiersModule[m.className]); - if (modifier instanceof Modifier.PokemonHeldItemModifier && modifier.pokemonId === pokemon.id) { - modifier.stackCount = m["stackCount"]; - heldItemsList.push(modifier); - } + let row = 0; + for (const [index, item] of pokemon.heldItemManager.getHeldItems().sort(heldItemSortFunc).entries()) { + if (index > 36) { + const overflowIcon = addTextObject(182, 4, "+", TextStyle.WINDOW); + heldItemsContainer.add(overflowIcon); + break; } - if (heldItemsList.length > 0) { - (heldItemsList as Modifier.PokemonHeldItemModifier[]).sort(Modifier.modifierSortFunc); - let row = 0; - for (const [index, item] of heldItemsList.entries()) { - if (index > 36) { - const overflowIcon = addTextObject(182, 4, "+", TextStyle.WINDOW); - heldItemsContainer.add(overflowIcon); - break; - } - const itemIcon = item?.getIcon(true); - if ( - item?.stackCount < item?.getMaxHeldItemCount(pokemon) && - itemIcon.list[1] instanceof Phaser.GameObjects.BitmapText - ) { - itemIcon.list[1].clearTint(); - } - itemIcon.setScale(heldItemsScale); - itemIcon.setPosition((index % 19) * 10, row * 10); - heldItemsContainer.add(itemIcon); - if (index !== 0 && index % 18 === 0) { - row++; - } - } + const itemIcon = allHeldItems[item].createSummaryIcon(pokemon); + if (!pokemon.heldItemManager.isMaxStack(item) && itemIcon.list[1] instanceof Phaser.GameObjects.BitmapText) { + itemIcon.list[1].clearTint(); + } + itemIcon.setScale(heldItemsScale); + itemIcon.setPosition((index % 19) * 10, row * 10); + heldItemsContainer.add(itemIcon); + if (index !== 0 && index % 18 === 0) { + row++; } } heldItemsContainer.setName("heldItems"); @@ -1142,7 +1121,7 @@ export default class RunInfoUiHandler extends UiHandler { } break; case Button.CYCLE_ABILITY: - if (this.runInfo.modifiers.length !== 0 && this.pageMode === RunInfoUiMode.MAIN) { + if (this.runInfo.trainerItems.length !== 0 && this.pageMode === RunInfoUiMode.MAIN) { if (this.partyVisibility) { this.showParty(false); } else { diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 7dee041fa63..139a4c7b853 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -2,8 +2,6 @@ import i18next from "i18next"; import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { GameMode } from "../game-mode"; -// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` -import * as Modifier from "#app/modifier/modifier"; import type { SessionSaveData } from "../system/game-data"; import type PokemonData from "../system/pokemon-data"; import { isNullOrUndefined, fixedInt, getPlayTimeString, formatLargeNumber } from "#app/utils/common"; @@ -12,6 +10,7 @@ import { TextStyle, addTextObject } from "./text"; import { UiMode } from "#enums/ui-mode"; import { addWindow } from "./ui-theme"; import { RunDisplayMode } from "#app/ui/run-info-ui-handler"; +import { allTrainerItems } from "#app/data/data-lists"; const SESSION_SLOTS_COUNT = 5; const SLOTS_ON_SCREEN = 3; @@ -443,12 +442,8 @@ class SessionSlot extends Phaser.GameObjects.Container { const modifierIconsContainer = globalScene.add.container(148, 30); modifierIconsContainer.setScale(0.5); let visibleModifierIndex = 0; - for (const m of data.modifiers) { - const modifier = m.toModifier(Modifier[m.className]); - if (modifier instanceof Modifier.PokemonHeldItemModifier) { - continue; - } - const icon = modifier?.getIcon(false); + for (const m of data.trainerItems) { + const icon = allTrainerItems[m.id].createIcon(m.stack); if (icon) { icon.setPosition(24 * visibleModifierIndex, 0); modifierIconsContainer.add(icon); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index f108faf1646..4b27e9bf144 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -25,7 +25,6 @@ import { MoveCategory } from "#enums/MoveCategory"; import { getPokeballAtlasKey } from "#app/data/pokeball"; import { getGenderColor, getGenderSymbol } from "#app/data/gender"; import { getLevelRelExp, getLevelTotalExp } from "#app/data/exp"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { StatusEffect } from "#enums/status-effect"; import { getBiomeName } from "#app/data/balance/biomes"; import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; @@ -35,11 +34,12 @@ import { getVariantTint } from "#app/sprites/variant"; import { Button } from "#enums/buttons"; import type { Ability } from "#app/data/abilities/ability"; import i18next from "i18next"; -import { modifierSortFunc } from "#app/modifier/modifier"; import { PlayerGender } from "#enums/player-gender"; import { Stat, PERMANENT_STATS, getStatKey } from "#enums/stat"; import { Nature } from "#enums/nature"; import { achvs } from "#app/system/achv"; +import { allHeldItems } from "#app/data/data-lists"; +import { heldItemSortFunc } from "#app/items/item-utility"; enum Page { PROFILE, @@ -1032,24 +1032,39 @@ export default class SummaryUiHandler extends UiHandler { }); this.ivContainer.setVisible(false); - const itemModifiers = ( - globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.pokemon?.id, - this.playerParty, - ) as PokemonHeldItemModifier[] - ).sort(modifierSortFunc); + const heldItems = this.pokemon?.getHeldItems().sort(heldItemSortFunc); - itemModifiers.forEach((item, i) => { - const icon = item.getIcon(true); + heldItems?.forEach((itemKey, i) => { + const heldItem = allHeldItems[itemKey]; + if (this.pokemon) { + const icon = heldItem.createSummaryIcon(this.pokemon); + + console.log(icon); + icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); + this.statsContainer.add(icon); + + icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); + icon.on("pointerover", () => globalScene.ui.showTooltip(heldItem.name, heldItem.description, true)); + icon.on("pointerout", () => globalScene.ui.hideTooltip()); + } + }); + /* + const formChangeItems = this.pokemon?.heldItemManager.getFormChangeItems().sort(formChangeItemSortFunc); + + //TODO: Make an equivalent function for form change items + formChangeItems?.forEach((itemKey, i) => { + const icon = heldItem.createSummaryIcon(stack); + + console.log(icon); icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); this.statsContainer.add(icon); icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); - icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true)); + icon.on("pointerover", () => globalScene.ui.showTooltip(heldItem.getName(), heldItem.getDescription(), true)); icon.on("pointerout", () => globalScene.ui.hideTooltip()); }); - +*/ const pkmLvl = this.pokemon?.level!; // TODO: is this bang correct? const pkmLvlExp = this.pokemon?.levelExp!; // TODO: is this bang correct? const pkmExp = this.pokemon?.exp!; // TODO: is this bang correct? diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index 8106e4de2da..87ea21e1c48 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -6,9 +6,9 @@ import { getMoveTargets } from "#app/data/moves/move-utils"; import { Button } from "#enums/buttons"; import type { MoveId } from "#enums/move-id"; import type Pokemon from "#app/field/pokemon"; -import type { ModifierBar } from "#app/modifier/modifier"; import { SubstituteTag } from "#app/data/battler-tags"; import { globalScene } from "#app/global-scene"; +import type { ModifierBar } from "#app/modifier/modifier-bar"; export type TargetSelectCallback = (targets: BattlerIndex[]) => void; diff --git a/src/ui/text.ts b/src/ui/text.ts index 76c85bac5cf..785af31a54b 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -4,7 +4,7 @@ import type Phaser from "phaser"; import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; import InputText from "phaser3-rex-plugins/plugins/inputtext"; import { globalScene } from "#app/global-scene"; -import { ModifierTier } from "../enums/modifier-tier"; +import { RewardTier } from "#app/enums/reward-tier"; import i18next from "#app/plugins/i18n"; export enum TextStyle { @@ -423,19 +423,19 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui } } -export function getModifierTierTextTint(tier: ModifierTier): number { +export function getModifierTierTextTint(tier: RewardTier): number { switch (tier) { - case ModifierTier.COMMON: + case RewardTier.COMMON: return 0xf8f8f8; - case ModifierTier.GREAT: + case RewardTier.GREAT: return 0x4998f8; - case ModifierTier.ULTRA: + case RewardTier.ULTRA: return 0xf8d038; - case ModifierTier.ROGUE: + case RewardTier.ROGUE: return 0xdb4343; - case ModifierTier.MASTER: + case RewardTier.MASTER: return 0xe331c5; - case ModifierTier.LUXURY: + case RewardTier.LUXURY: return 0xe74c18; } } @@ -443,12 +443,12 @@ export function getModifierTierTextTint(tier: ModifierTier): number { export function getEggTierTextTint(tier: EggTier): number { switch (tier) { case EggTier.COMMON: - return getModifierTierTextTint(ModifierTier.COMMON); + return getModifierTierTextTint(RewardTier.COMMON); case EggTier.RARE: - return getModifierTierTextTint(ModifierTier.GREAT); + return getModifierTierTextTint(RewardTier.GREAT); case EggTier.EPIC: - return getModifierTierTextTint(ModifierTier.ULTRA); + return getModifierTierTextTint(RewardTier.ULTRA); case EggTier.LEGENDARY: - return getModifierTierTextTint(ModifierTier.MASTER); + return getModifierTierTextTint(RewardTier.MASTER); } } diff --git a/src/utils/common.ts b/src/utils/common.ts index 753d6ebb865..2e9f429f1c7 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -658,3 +658,22 @@ export function enumValueToKey>(input: } throw new Error(`Invalid value passed to \`enumValueToKey\`! Value: ${val}`); } + +export function pickWeightedIndex(weights: number[]): number | undefined { + const totalWeight = weights.reduce((sum, w) => sum + w, 0); + + if (totalWeight <= 0) { + return undefined; + } + + let r = randSeedFloat() * totalWeight; + + for (let i = 0; i < weights.length; i++) { + if (r < weights[i]) { + return i; + } + r -= weights[i]; + } + + return undefined; // TODO: Change to something more appropriate +} diff --git a/src/utils/modifier-utils.ts b/src/utils/modifier-utils.ts index 3be4af3730c..5093d96b5ba 100644 --- a/src/utils/modifier-utils.ts +++ b/src/utils/modifier-utils.ts @@ -1,11 +1,5 @@ import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { - dailyStarterModifierPool, - enemyBuffModifierPool, - modifierPool, - trainerModifierPool, - wildModifierPool, -} from "#app/modifier/modifier-pools"; +import { modifierPool } from "#app/modifier/modifier-pools"; import type { ModifierPool, ModifierTypeFunc } from "#app/@types/modifier-types"; import { modifierTypes } from "#app/data/data-lists"; import type { ModifierType } from "#app/modifier/modifier-type"; @@ -14,14 +8,6 @@ export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool switch (poolType) { case ModifierPoolType.PLAYER: return modifierPool; - case ModifierPoolType.WILD: - return wildModifierPool; - case ModifierPoolType.TRAINER: - return trainerModifierPool; - case ModifierPoolType.ENEMY_BUFF: - return enemyBuffModifierPool; - case ModifierPoolType.DAILY_STARTER: - return dailyStarterModifierPool; } } diff --git a/test/abilities/cud_chew.test.ts b/test/abilities/cud_chew.test.ts index e563e7537dd..b8e2ae7e812 100644 --- a/test/abilities/cud_chew.test.ts +++ b/test/abilities/cud_chew.test.ts @@ -4,6 +4,7 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { AbilityId } from "#enums/ability-id"; import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -30,7 +31,7 @@ describe("Abilities - Cud Chew", () => { game = new GameManager(phaserGame); game.override .moveset([MoveId.BUG_BITE, MoveId.SPLASH, MoveId.HYPER_VOICE, MoveId.STUFF_CHEEKS]) - .startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]) + .startingHeldItems([{ entry: HeldItemId.SITRUS_BERRY }]) .ability(AbilityId.CUD_CHEW) .battleStyle("single") .criticalHits(false) @@ -114,8 +115,8 @@ describe("Abilities - Cud Chew", () => { vi.spyOn(Pokemon.prototype, "randBattleSeedInt").mockReturnValue(0); game.override .startingHeldItems([ - { name: "BERRY", type: BerryType.PETAYA, count: 3 }, - { name: "BERRY", type: BerryType.LIECHI, count: 3 }, + { entry: HeldItemId.PETAYA_BERRY, count: 3 }, + { entry: HeldItemId.LIECHI_BERRY, count: 3 }, ]) .enemyMoveset(MoveId.TEATIME); await game.classicMode.startBattle([SpeciesId.FARIGIRAF]); @@ -258,7 +259,7 @@ describe("Abilities - Cud Chew", () => { it("works with pluck", async () => { game.override .enemySpecies(SpeciesId.BLAZIKEN) - .enemyHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]) + .enemyHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) .startingHeldItems([]); await game.classicMode.startBattle([SpeciesId.FARIGIRAF]); diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts index 42c9772bd10..0cf2d712368 100644 --- a/test/abilities/harvest.test.ts +++ b/test/abilities/harvest.test.ts @@ -1,7 +1,6 @@ import { BattlerIndex } from "#enums/battler-index"; import { PostTurnRestoreBerryAbAttr } from "#app/data/abilities/ability"; import type Pokemon from "#app/field/pokemon"; -import { BerryModifier, PreserveBerryModifier } from "#app/modifier/modifier"; import type { ModifierOverride } from "#app/modifier/modifier-type"; import type { BooleanHolder } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; @@ -13,6 +12,7 @@ import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { HeldItemId } from "#enums/held-item-id"; describe("Abilities - Harvest", () => { let phaserGame: Phaser.Game; @@ -57,7 +57,7 @@ describe("Abilities - Harvest", () => { }); it("replenishes eaten berries", async () => { - game.override.startingHeldItems([{ name: "BERRY", type: BerryType.LUM, count: 1 }]); + game.override.startingHeldItems([{ entry: HeldItemId.LUM_BERRY }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -117,9 +117,8 @@ describe("Abilities - Harvest", () => { }); it("remembers berries eaten array across waves", async () => { - game.override - .startingHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 2 }]) - .ability(AbilityId.BALL_FETCH); // don't actually need harvest for this test + game.override; + game.override.startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY, count: 2 }]).ability(AbilityId.BALL_FETCH); // don't actually need harvest for this test await game.classicMode.startBattle([SpeciesId.REGIELEKI]); const regieleki = game.scene.getPlayerPokemon()!; @@ -144,7 +143,7 @@ describe("Abilities - Harvest", () => { it("keeps harvested berries across reloads", async () => { game.override - .startingHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]) + .startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) .moveset([MoveId.SPLASH, MoveId.EARTHQUAKE]) .enemyMoveset([MoveId.SUPER_FANG, MoveId.HEAL_PULSE]) .enemyAbility(AbilityId.COMPOUND_EYES); @@ -180,11 +179,10 @@ describe("Abilities - Harvest", () => { }); it("cannot restore capped berries", async () => { - const initBerries: ModifierOverride[] = [ - { name: "BERRY", type: BerryType.LUM, count: 2 }, - { name: "BERRY", type: BerryType.STARF, count: 2 }, - ]; - game.override.startingHeldItems(initBerries); + game.override.startingHeldItems([ + { entry: HeldItemId.LUM_BERRY, count: 2 }, + { entry: HeldItemId.STARF_BERRY, count: 2 }, + ]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); const feebas = game.scene.getPlayerPokemon()!; @@ -227,7 +225,7 @@ describe("Abilities - Harvest", () => { describe("move/ability interactions", () => { it("cannot restore incinerated berries", async () => { - game.override.startingHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 3 }]); + game.override.startingHeldItems([{ entry: HeldItemId.STARF_BERRY, count: 3 }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -249,8 +247,7 @@ describe("Abilities - Harvest", () => { }); it("can restore berries eaten by Teatime", async () => { - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.STARF, count: 1 }]; - game.override.startingHeldItems(initBerries).enemyMoveset(MoveId.TEATIME); + game.override.startingHeldItems([{ entry: HeldItemId.STARF_BERRY }]).enemyMoveset(MoveId.TEATIME); await game.classicMode.startBattle([SpeciesId.FEEBAS]); // nom nom the berr berr yay yay @@ -262,8 +259,10 @@ describe("Abilities - Harvest", () => { }); it("cannot restore Plucked berries for either side", async () => { - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]; - game.override.startingHeldItems(initBerries).enemyAbility(AbilityId.HARVEST).enemyMoveset(MoveId.PLUCK); + game.override + .startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) + .enemyAbility(AbilityId.HARVEST) + .enemyMoveset(MoveId.PLUCK); await game.classicMode.startBattle([SpeciesId.FEEBAS]); // gobble gobble gobble @@ -285,8 +284,9 @@ describe("Abilities - Harvest", () => { }, ); - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]; - game.override.startingHeldItems(initBerries).startingModifier([{ name: "BERRY_POUCH", count: 1 }]); + game.override + .startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) + .startingModifier([{ name: "BERRY_POUCH", count: 1 }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -320,7 +320,7 @@ describe("Abilities - Harvest", () => { // TODO: Enable once fling actually works...??? it.todo("can restore berries flung at user", async () => { - game.override.enemyHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 1 }]).enemyMoveset(MoveId.FLING); + game.override.enemyHeldItems([{ entry: HeldItemId.STARF_BERRY }]).enemyMoveset(MoveId.FLING); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -332,8 +332,7 @@ describe("Abilities - Harvest", () => { // TODO: Enable once Nat Gift gets implemented...??? it.todo("can restore berries consumed via Natural Gift", async () => { - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.STARF, count: 1 }]; - game.override.startingHeldItems(initBerries); + game.override.startingHeldItems([{ entry: HeldItemId.STARF_BERRY }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.NATURAL_GIFT); diff --git a/test/abilities/unburden.test.ts b/test/abilities/unburden.test.ts index fff37daff7b..c67703dcec3 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -2,16 +2,16 @@ import { BattlerIndex } from "#enums/battler-index"; import { StealHeldItemChanceAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import type Pokemon from "#app/field/pokemon"; -import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { HeldItemId } from "#enums/held-item-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; describe("Abilities - Unburden", () => { let phaserGame: Phaser.Game; @@ -21,7 +21,7 @@ describe("Abilities - Unburden", () => { * 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()); + const stackCounts = pokemon.getHeldItems().map(t => pokemon.heldItemManager.getStack(t)); return stackCounts.reduce((a, b) => a + b, 0); } @@ -43,19 +43,16 @@ describe("Abilities - Unburden", () => { .ability(AbilityId.UNBURDEN) .moveset([MoveId.SPLASH, MoveId.KNOCK_OFF, MoveId.PLUCK, MoveId.FALSE_SWIPE]) .startingHeldItems([ - { name: "BERRY", count: 1, type: BerryType.SITRUS }, - { name: "BERRY", count: 2, type: BerryType.APICOT }, - { name: "BERRY", count: 2, type: BerryType.LUM }, + { entry: HeldItemId.SITRUS_BERRY }, + { entry: HeldItemId.APICOT_BERRY, count: 2 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ]) .enemySpecies(SpeciesId.NINJASK) .enemyLevel(100) .enemyMoveset(MoveId.SPLASH) .enemyAbility(AbilityId.UNBURDEN) .enemyPassiveAbility(AbilityId.NO_GUARD) - .enemyHeldItems([ - { name: "BERRY", type: BerryType.SITRUS, count: 1 }, - { name: "BERRY", type: BerryType.LUM, count: 1 }, - ]); + .enemyHeldItems([{ entry: HeldItemId.SITRUS_BERRY }, { entry: HeldItemId.LUM_BERRY }]); // For the various tests that use Thief, give it a 100% steal rate vi.spyOn(allMoves[MoveId.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); }); @@ -77,7 +74,9 @@ describe("Abilities - Unburden", () => { }); it("should activate when a berry is eaten, even if Berry Pouch preserves the berry", async () => { - game.override.enemyMoveset(MoveId.FALSE_SWIPE).startingModifier([{ name: "BERRY_POUCH", count: 5850 }]); + game.override + .enemyMoveset(MoveId.FALSE_SWIPE) + .startingHeldItems([{ entry: TrainerItemId.BERRY_POUCH, count: 5850 }]); await game.classicMode.startBattle([SpeciesId.TREECKO]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -174,13 +173,9 @@ describe("Abilities - Unburden", () => { }); it("should activate when an item is stolen via grip claw", async () => { - game.override.startingHeldItems([{ name: "GRIP_CLAW", count: 1 }]); + game.override.startingHeldItems([{ entry: HeldItemId.GRIP_CLAW, count: 10 }]); await game.classicMode.startBattle([SpeciesId.TREECKO]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; - vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); - const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); @@ -267,7 +262,7 @@ describe("Abilities - Unburden", () => { }); it("should not activate when passing a baton to a teammate switching in", async () => { - game.override.startingHeldItems([{ name: "BATON" }]).moveset(MoveId.BATON_PASS); + game.override.startingHeldItems([{ entry: HeldItemId.BATON }]).moveset(MoveId.BATON_PASS); await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.PURRLOIN]); const [treecko, purrloin] = game.scene.getPlayerParty(); @@ -314,7 +309,7 @@ describe("Abilities - Unburden", () => { }); it("should activate when a reviver seed is used", async () => { - game.override.startingHeldItems([{ name: "REVIVER_SEED" }]).enemyMoveset([MoveId.WING_ATTACK]); + game.override.startingHeldItems([{ entry: HeldItemId.REVIVER_SEED }]).enemyMoveset([MoveId.WING_ATTACK]); await game.classicMode.startBattle([SpeciesId.TREECKO]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -359,7 +354,7 @@ describe("Abilities - Unburden", () => { .battleStyle("double") .enemyMoveset([MoveId.SPLASH, MoveId.THIEF]) .moveset([MoveId.SPLASH, MoveId.REVIVAL_BLESSING]) - .startingHeldItems([{ name: "WIDE_LENS" }]); + .startingHeldItems([{ entry: HeldItemId.WIDE_LENS }]); await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.FEEBAS, SpeciesId.MILOTIC]); const treecko = game.scene.getPlayerField()[0]; diff --git a/test/items/dire_hit.test.ts b/test/items/dire_hit.test.ts index a484a0ad302..ce1ff4cd801 100644 --- a/test/items/dire_hit.test.ts +++ b/test/items/dire_hit.test.ts @@ -5,7 +5,6 @@ import GameManager from "#test/testUtils/gameManager"; import Phase from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; -import { TempCritBoosterModifier } from "#app/modifier/modifier"; import { UiMode } from "#enums/ui-mode"; import type ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { Button } from "#app/enums/buttons"; @@ -13,6 +12,7 @@ import { CommandPhase } from "#app/phases/command-phase"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; +import { TrainerItemId } from "#enums/trainer-item-id"; describe("Items - Dire Hit", () => { let phaserGame: Phaser.Game; @@ -35,7 +35,7 @@ describe("Items - Dire Hit", () => { .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.SPLASH) .moveset([MoveId.POUND]) - .startingHeldItems([{ name: "DIRE_HIT" }]) + .startingTrainerItems([{ entry: TrainerItemId.DIRE_HIT }]) .battleStyle("single"); }); @@ -63,8 +63,8 @@ describe("Items - Dire Hit", () => { await game.phaseInterceptor.to(BattleEndPhase); - const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier; - expect(modifier.getBattleCount()).toBe(4); + const stack = game.scene.trainerItems.getStack(TrainerItemId.DIRE_HIT); + expect(stack).toBe(4); // Forced DIRE_HIT to spawn in the first slot with override game.onNextPrompt( @@ -83,14 +83,7 @@ describe("Items - Dire Hit", () => { await game.phaseInterceptor.to(TurnInitPhase); - // Making sure only one booster is in the modifier list even after picking up another - let count = 0; - for (const m of game.scene.modifiers) { - if (m instanceof TempCritBoosterModifier) { - count++; - expect((m as TempCritBoosterModifier).getBattleCount()).toBe(5); - } - } - expect(count).toBe(1); + const newStack = game.scene.trainerItems.getStack(TrainerItemId.DIRE_HIT); + expect(newStack).toBe(5); }); }); diff --git a/test/items/double_battle_chance_booster.test.ts b/test/items/double_battle_chance_booster.test.ts index d1a9e826cda..6b19cf37266 100644 --- a/test/items/double_battle_chance_booster.test.ts +++ b/test/items/double_battle_chance_booster.test.ts @@ -1,6 +1,5 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { DoubleBattleChanceBoosterModifier } from "#app/modifier/modifier"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -8,6 +7,7 @@ import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import { UiMode } from "#enums/ui-mode"; import type ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { Button } from "#app/enums/buttons"; +import { TrainerItemId } from "#enums/trainer-item-id"; describe("Items - Double Battle Chance Boosters", () => { let phaserGame: Phaser.Game; @@ -27,7 +27,9 @@ describe("Items - Double Battle Chance Boosters", () => { }); it("should guarantee double battle with 2 unique tiers", async () => { - game.override.startingModifier([{ name: "LURE" }, { name: "SUPER_LURE" }]).startingWave(2); + game.override + .startingTrainerItems([{ entry: TrainerItemId.LURE }, { entry: TrainerItemId.SUPER_LURE }]) + .startingWave(2); await game.classicMode.startBattle(); @@ -35,7 +37,13 @@ describe("Items - Double Battle Chance Boosters", () => { }); it("should guarantee double boss battle with 3 unique tiers", async () => { - game.override.startingModifier([{ name: "LURE" }, { name: "SUPER_LURE" }, { name: "MAX_LURE" }]).startingWave(10); + game.override + .startingTrainerItems([ + { entry: TrainerItemId.LURE }, + { entry: TrainerItemId.SUPER_LURE }, + { entry: TrainerItemId.MAX_LURE }, + ]) + .startingWave(10); await game.classicMode.startBattle(); @@ -48,7 +56,7 @@ describe("Items - Double Battle Chance Boosters", () => { it("should renew how many battles are left of existing booster when picking up new booster of same tier", async () => { game.override - .startingModifier([{ name: "LURE" }]) + .startingTrainerItems([{ entry: TrainerItemId.LURE }]) .itemRewards([{ name: "LURE" }]) .moveset(MoveId.SPLASH) .startingLevel(200); @@ -61,10 +69,8 @@ describe("Items - Double Battle Chance Boosters", () => { await game.phaseInterceptor.to("BattleEndPhase"); - const modifier = game.scene.findModifier( - m => m instanceof DoubleBattleChanceBoosterModifier, - ) as DoubleBattleChanceBoosterModifier; - expect(modifier.getBattleCount()).toBe(9); + const stack = game.scene.trainerItems.getStack(TrainerItemId.LURE); + expect(stack).toBe(9); // Forced LURE to spawn in the first slot with override game.onNextPrompt( @@ -84,14 +90,7 @@ describe("Items - Double Battle Chance Boosters", () => { await game.phaseInterceptor.to("TurnInitPhase"); // Making sure only one booster is in the modifier list even after picking up another - let count = 0; - for (const m of game.scene.modifiers) { - if (m instanceof DoubleBattleChanceBoosterModifier) { - count++; - const modifierInstance = m as DoubleBattleChanceBoosterModifier; - expect(modifierInstance.getBattleCount()).toBe(modifierInstance.getMaxBattles()); - } - } - expect(count).toBe(1); + const newStack = game.scene.trainerItems.getStack(TrainerItemId.LURE); + expect(newStack).toBe(10); }); }); diff --git a/test/items/eviolite.test.ts b/test/items/eviolite.test.ts index 353cdbf91b4..0539280e132 100644 --- a/test/items/eviolite.test.ts +++ b/test/items/eviolite.test.ts @@ -1,5 +1,7 @@ -import { StatBoosterModifier } from "#app/modifier/modifier"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; import { NumberHolder, randItem } from "#app/utils/common"; +import { HeldItemId } from "#enums/held-item-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; @@ -22,7 +24,7 @@ describe("Items - Eviolite", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single").startingHeldItems([{ name: "EVIOLITE" }]); + game.override.battleStyle("single").startingHeldItems([{ entry: HeldItemId.EVIOLITE }]); }); it("should provide 50% boost to DEF and SPDEF for unevolved, unfused pokemon", async () => { @@ -32,9 +34,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); - - // Ignore other calculations for simplicity + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); return Math.floor(statValue.value); }); @@ -53,7 +53,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -83,7 +83,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -113,7 +113,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -143,7 +143,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -173,7 +173,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -203,7 +203,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity diff --git a/test/items/exp_booster.test.ts b/test/items/exp_booster.test.ts index 44d7721aba2..ded5cb85544 100644 --- a/test/items/exp_booster.test.ts +++ b/test/items/exp_booster.test.ts @@ -1,9 +1,11 @@ import { AbilityId } from "#enums/ability-id"; -import { PokemonExpBoosterModifier } from "#app/modifier/modifier"; import { NumberHolder } from "#app/utils/common"; import GameManager from "#test/testUtils/gameManager"; import Phase from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { HeldItemId } from "#enums/held-item-id"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; describe("EXP Modifier Items", () => { let phaserGame: Phaser.Game; @@ -26,13 +28,13 @@ describe("EXP Modifier Items", () => { }); it("EXP booster items stack multiplicatively", async () => { - game.override.startingHeldItems([{ name: "LUCKY_EGG", count: 3 }, { name: "GOLDEN_EGG" }]); + game.override.startingHeldItems([{ entry: HeldItemId.LUCKY_EGG, count: 3 }, { entry: HeldItemId.GOLDEN_EGG }]); await game.classicMode.startBattle(); const partyMember = game.scene.getPlayerPokemon()!; partyMember.exp = 100; const expHolder = new NumberHolder(partyMember.exp); - game.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, expHolder); + applyHeldItems(HELD_ITEM_EFFECT.EXP_BOOSTER, { pokemon: partyMember, expAmount: expHolder }); expect(expHolder.value).toBe(440); }); }); diff --git a/test/items/grip_claw.test.ts b/test/items/grip_claw.test.ts index 9c3e6548140..736481b523b 100644 --- a/test/items/grip_claw.test.ts +++ b/test/items/grip_claw.test.ts @@ -1,13 +1,11 @@ import { BattlerIndex } from "#enums/battler-index"; -import type Pokemon from "#app/field/pokemon"; -import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; import { AbilityId } from "#enums/ability-id"; -import { BerryType } from "#enums/berry-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phase from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { HeldItemId } from "#enums/held-item-id"; describe("Items - Grip Claw", () => { let phaserGame: Phaser.Game; @@ -29,14 +27,14 @@ describe("Items - Grip Claw", () => { game.override .battleStyle("double") .moveset([MoveId.TACKLE, MoveId.SPLASH, MoveId.ATTRACT]) - .startingHeldItems([{ name: "GRIP_CLAW", count: 1 }]) + .startingHeldItems([{ entry: HeldItemId.GRIP_CLAW }]) .enemySpecies(SpeciesId.SNORLAX) .enemyAbility(AbilityId.UNNERVE) .ability(AbilityId.UNNERVE) .enemyMoveset(MoveId.SPLASH) .enemyHeldItems([ - { name: "BERRY", type: BerryType.SITRUS, count: 2 }, - { name: "BERRY", type: BerryType.LUM, count: 2 }, + { entry: HeldItemId.SITRUS_BERRY, count: 2 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ]) .enemyLevel(100); }); @@ -45,15 +43,12 @@ describe("Items - Grip Claw", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); const [playerPokemon] = game.scene.getPlayerField(); - - const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; - vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); - const enemyPokemon = game.scene.getEnemyField(); + playerPokemon.heldItemManager.setStack(HeldItemId.GRIP_CLAW, 10); - const playerHeldItemCount = getHeldItemCount(playerPokemon); - const enemy1HeldItemCount = getHeldItemCount(enemyPokemon[0]); - const enemy2HeldItemCount = getHeldItemCount(enemyPokemon[1]); + const playerHeldItemCount = playerPokemon.heldItemManager.getHeldItemCount(); + const enemy1HeldItemCount = enemyPokemon[0].heldItemManager.getHeldItemCount(); + const enemy2HeldItemCount = enemyPokemon[1].heldItemManager.getHeldItemCount(); expect(enemy2HeldItemCount).toBeGreaterThan(0); game.move.select(MoveId.TACKLE, 0, BattlerIndex.ENEMY_2); @@ -61,9 +56,9 @@ describe("Items - Grip Claw", () => { await game.phaseInterceptor.to("BerryPhase", false); - const playerHeldItemCountAfter = getHeldItemCount(playerPokemon); - const enemy1HeldItemCountsAfter = getHeldItemCount(enemyPokemon[0]); - const enemy2HeldItemCountsAfter = getHeldItemCount(enemyPokemon[1]); + const playerHeldItemCountAfter = playerPokemon.heldItemManager.getHeldItemCount(); + const enemy1HeldItemCountsAfter = enemyPokemon[0].heldItemManager.getHeldItemCount(); + const enemy2HeldItemCountsAfter = enemyPokemon[1].heldItemManager.getHeldItemCount(); expect(playerHeldItemCountAfter).toBe(playerHeldItemCount + 1); expect(enemy1HeldItemCountsAfter).toBe(enemy1HeldItemCount); @@ -74,15 +69,11 @@ describe("Items - Grip Claw", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); const [playerPokemon] = game.scene.getPlayerField(); - - const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; - vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); - const enemyPokemon = game.scene.getEnemyField(); - const playerHeldItemCount = getHeldItemCount(playerPokemon); - const enemy1HeldItemCount = getHeldItemCount(enemyPokemon[0]); - const enemy2HeldItemCount = getHeldItemCount(enemyPokemon[1]); + const playerHeldItemCount = playerPokemon.heldItemManager.getHeldItemCount(); + const enemy1HeldItemCount = enemyPokemon[0].heldItemManager.getHeldItemCount(); + const enemy2HeldItemCount = enemyPokemon[1].heldItemManager.getHeldItemCount(); expect(enemy2HeldItemCount).toBeGreaterThan(0); game.move.select(MoveId.ATTRACT, 0, BattlerIndex.ENEMY_2); @@ -90,9 +81,9 @@ describe("Items - Grip Claw", () => { await game.phaseInterceptor.to("BerryPhase", false); - const playerHeldItemCountAfter = getHeldItemCount(playerPokemon); - const enemy1HeldItemCountsAfter = getHeldItemCount(enemyPokemon[0]); - const enemy2HeldItemCountsAfter = getHeldItemCount(enemyPokemon[1]); + const playerHeldItemCountAfter = playerPokemon.heldItemManager.getHeldItemCount(); + const enemy1HeldItemCountsAfter = enemyPokemon[0].heldItemManager.getHeldItemCount(); + const enemy2HeldItemCountsAfter = enemyPokemon[1].heldItemManager.getHeldItemCount(); expect(playerHeldItemCountAfter).toBe(playerHeldItemCount); expect(enemy1HeldItemCountsAfter).toBe(enemy1HeldItemCount); @@ -103,31 +94,19 @@ describe("Items - Grip Claw", () => { game.override .battleStyle("double") .moveset([MoveId.POLLEN_PUFF, MoveId.ENDURE]) - .startingHeldItems([ - { name: "GRIP_CLAW", count: 1 }, - { name: "BERRY", type: BerryType.LUM, count: 1 }, - ]); + .startingHeldItems([{ entry: HeldItemId.GRIP_CLAW }, { entry: HeldItemId.LUM_BERRY }]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.OMANYTE]); const [leftPokemon, rightPokemon] = game.scene.getPlayerField(); + leftPokemon.heldItemManager.setStack(HeldItemId.GRIP_CLAW, 10); - const gripClaw = leftPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; - vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); - - const heldItemCountBefore = getHeldItemCount(rightPokemon); + const heldItemCountBefore = rightPokemon.heldItemManager.getHeldItemCount(); game.move.select(MoveId.POLLEN_PUFF, 0, BattlerIndex.PLAYER_2); game.move.select(MoveId.ENDURE, 1); await game.toNextTurn(); - expect(getHeldItemCount(rightPokemon)).toBe(heldItemCountBefore); + expect(rightPokemon.heldItemManager.getHeldItemCount()).toBe(heldItemCountBefore); }); }); - -/* - * Gets the total number of items a Pokemon holds - */ -function getHeldItemCount(pokemon: Pokemon) { - return pokemon.getHeldItems().reduce((currentTotal, item) => currentTotal + item.getStackCount(), 0); -} diff --git a/test/items/leek.test.ts b/test/items/leek.test.ts index eedb6667b9b..058790ef7fa 100644 --- a/test/items/leek.test.ts +++ b/test/items/leek.test.ts @@ -1,5 +1,6 @@ import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { randInt } from "#app/utils/common"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; @@ -26,7 +27,7 @@ describe("Items - Leek", () => { game.override .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.SPLASH) - .startingHeldItems([{ name: "LEEK" }]) + .startingHeldItems([{ entry: HeldItemId.LEEK }]) .moveset([MoveId.TACKLE]) .battleStyle("single"); }); diff --git a/test/items/leftovers.test.ts b/test/items/leftovers.test.ts index 21319d2c9d7..30cbe6434ba 100644 --- a/test/items/leftovers.test.ts +++ b/test/items/leftovers.test.ts @@ -1,6 +1,7 @@ import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; @@ -31,17 +32,17 @@ describe("Items - Leftovers", () => { .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.UNNERVE) .enemyMoveset(MoveId.TACKLE) - .startingHeldItems([{ name: "LEFTOVERS", count: 1 }]); + .startingHeldItems([{ entry: HeldItemId.LEFTOVERS }]); }); it("leftovers works", async () => { await game.classicMode.startBattle([SpeciesId.ARCANINE]); - // Make sure leftovers are there - expect(game.scene.modifiers[0].type.id).toBe("LEFTOVERS"); - const leadPokemon = game.scene.getPlayerPokemon()!; + // Make sure leftovers are there + expect(leadPokemon.heldItemManager.hasItem(HeldItemId.LEFTOVERS)).toBe(true); + // We should have full hp expect(leadPokemon.isFullHp()).toBe(true); diff --git a/test/items/light_ball.test.ts b/test/items/light_ball.test.ts index 6dfed3389b9..742e7dc8d61 100644 --- a/test/items/light_ball.test.ts +++ b/test/items/light_ball.test.ts @@ -1,12 +1,13 @@ import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import { NumberHolder } from "#app/utils/common"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phase from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { HeldItemId } from "#enums/held-item-id"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { HELD_ITEM_EFFECT } from "#app/items/held-item"; describe("Items - Light Ball", () => { let phaserGame: Phaser.Game; @@ -29,7 +30,7 @@ describe("Items - Light Ball", () => { }); it("LIGHT_BALL activates in battle correctly", async () => { - game.override.startingHeldItems([{ name: "RARE_SPECIES_STAT_BOOSTER", type: "LIGHT_BALL" }]); + game.override.startingHeldItems([{ entry: HeldItemId.LIGHT_BALL }]); const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.PIKACHU]); @@ -91,20 +92,17 @@ describe("Items - Light Ball", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: atkValue }); const spAtkValue = new NumberHolder(spAtkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue); - + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPDEF, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["LIGHT_BALL"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); + // Giving Light Ball to party member and testing if it applies + partyMember.heldItemManager.add(HeldItemId.LIGHT_BALL); + + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPATK, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); @@ -130,20 +128,18 @@ describe("Items - Light Ball", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: atkValue }); const spAtkValue = new NumberHolder(spAtkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPDEF, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["LIGHT_BALL"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); + // Giving Light Ball to party member and testing if it applies + partyMember.heldItemManager.add(HeldItemId.LIGHT_BALL); + + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPATK, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); @@ -169,20 +165,18 @@ describe("Items - Light Ball", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: atkValue }); const spAtkValue = new NumberHolder(spAtkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPDEF, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["LIGHT_BALL"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); + // Giving Light Ball to party member and testing if it applies + partyMember.heldItemManager.add(HeldItemId.LIGHT_BALL); + + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPATK, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); @@ -198,20 +192,17 @@ describe("Items - Light Ball", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: atkValue }); const spAtkValue = new NumberHolder(spAtkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPDEF, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["LIGHT_BALL"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); + // Giving Light Ball to party member and testing if it applies + partyMember.heldItemManager.add(HeldItemId.LIGHT_BALL); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); + applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPATK, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); diff --git a/test/items/lock_capsule.test.ts b/test/items/lock_capsule.test.ts index beacc3a3907..0395d775019 100644 --- a/test/items/lock_capsule.test.ts +++ b/test/items/lock_capsule.test.ts @@ -1,11 +1,12 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { UiMode } from "#enums/ui-mode"; import GameManager from "#test/testUtils/gameManager"; import Phase from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { TrainerItemId } from "#enums/trainer-item-id"; describe("Items - Lock Capsule", () => { let phaserGame: Phaser.Game; @@ -29,14 +30,14 @@ describe("Items - Lock Capsule", () => { .startingLevel(200) .moveset([MoveId.SURF]) .enemyAbility(AbilityId.BALL_FETCH) - .startingModifier([{ name: "LOCK_CAPSULE" }]); + .startingTrainerItems([{ entry: TrainerItemId.LOCK_CAPSULE }]); }); it("doesn't set the cost of common tier items to 0", async () => { await game.classicMode.startBattle(); game.scene.phaseManager.overridePhase( new SelectModifierPhase(0, undefined, { - guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.COMMON], + guaranteedModifierTiers: [RewardTier.COMMON, RewardTier.COMMON, RewardTier.COMMON], fillRemaining: false, }), ); diff --git a/test/items/mystical_rock.test.ts b/test/items/mystical_rock.test.ts index 091815aa604..acbd9d50a32 100644 --- a/test/items/mystical_rock.test.ts +++ b/test/items/mystical_rock.test.ts @@ -5,6 +5,7 @@ import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phase from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { HeldItemId } from "#enums/held-item-id"; describe("Items - Mystical Rock", () => { let phaserGame: Phaser.Game; @@ -28,7 +29,7 @@ describe("Items - Mystical Rock", () => { .enemyMoveset(MoveId.SPLASH) .enemyAbility(AbilityId.BALL_FETCH) .moveset([MoveId.SUNNY_DAY, MoveId.GRASSY_TERRAIN]) - .startingHeldItems([{ name: "MYSTICAL_ROCK", count: 2 }]) + .startingHeldItems([{ entry: HeldItemId.MYSTICAL_ROCK, count: 2 }]) .battleStyle("single"); }); diff --git a/test/items/reviver_seed.test.ts b/test/items/reviver_seed.test.ts index f444a6eac66..d977189789c 100644 --- a/test/items/reviver_seed.test.ts +++ b/test/items/reviver_seed.test.ts @@ -1,13 +1,13 @@ import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { BattlerTagType } from "#app/enums/battler-tag-type"; -import type { PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { HeldItemId } from "#enums/held-item-id"; describe("Items - Reviver Seed", () => { let phaserGame: Phaser.Game; @@ -32,8 +32,8 @@ describe("Items - Reviver Seed", () => { .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) - .startingHeldItems([{ name: "REVIVER_SEED" }]) - .enemyHeldItems([{ name: "REVIVER_SEED" }]) + .startingHeldItems([{ entry: HeldItemId.REVIVER_SEED }]) + .enemyHeldItems([{ entry: HeldItemId.REVIVER_SEED }]) .enemyMoveset(MoveId.SPLASH); vi.spyOn(allMoves[MoveId.SHEER_COLD], "accuracy", "get").mockReturnValue(100); vi.spyOn(allMoves[MoveId.LEECH_SEED], "accuracy", "get").mockReturnValue(100); diff --git a/test/items/scope_lens.test.ts b/test/items/scope_lens.test.ts index 16be8aab930..6e7712bac2f 100644 --- a/test/items/scope_lens.test.ts +++ b/test/items/scope_lens.test.ts @@ -1,4 +1,5 @@ import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; @@ -26,7 +27,7 @@ describe("Items - Scope Lens", () => { .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.SPLASH) .moveset([MoveId.POUND]) - .startingHeldItems([{ name: "SCOPE_LENS" }]) + .startingHeldItems([{ entry: HeldItemId.SCOPE_LENS }]) .battleStyle("single"); }); diff --git a/test/items/temp_stat_stage_booster.test.ts b/test/items/temp_stat_stage_booster.test.ts index b8cd0cde4eb..621697f441d 100644 --- a/test/items/temp_stat_stage_booster.test.ts +++ b/test/items/temp_stat_stage_booster.test.ts @@ -6,7 +6,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite import { MoveId } from "#enums/move-id"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { AbilityId } from "#enums/ability-id"; -import { TempStatStageBoosterModifier } from "#app/modifier/modifier"; import { UiMode } from "#enums/ui-mode"; import { Button } from "#app/enums/buttons"; import type ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; diff --git a/test/items/toxic_orb.test.ts b/test/items/toxic_orb.test.ts index e0d86655028..36ccb073318 100644 --- a/test/items/toxic_orb.test.ts +++ b/test/items/toxic_orb.test.ts @@ -1,5 +1,6 @@ import i18next from "#app/plugins/i18n"; import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; @@ -30,11 +31,7 @@ describe("Items - Toxic orb", () => { .enemyAbility(AbilityId.BALL_FETCH) .moveset(MoveId.SPLASH) .enemyMoveset(MoveId.SPLASH) - .startingHeldItems([ - { - name: "TOXIC_ORB", - }, - ]); + .startingHeldItems([{ entry: HeldItemId.TOXIC_ORB }]); vi.spyOn(i18next, "t"); }); @@ -43,7 +40,7 @@ describe("Items - Toxic orb", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); const player = game.scene.getPlayerPokemon()!; - expect(player.getHeldItems()[0].type.id).toBe("TOXIC_ORB"); + expect(player.heldItemManager.hasItem(HeldItemId.TOXIC_ORB)).toBe(true); game.move.select(MoveId.SPLASH); diff --git a/test/moves/fusion_flare_bolt.test.ts b/test/moves/fusion_flare_bolt.test.ts index 1967e9f12d1..d7af700d39b 100644 --- a/test/moves/fusion_flare_bolt.test.ts +++ b/test/moves/fusion_flare_bolt.test.ts @@ -165,7 +165,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Get rid of any modifiers that may alter power game.scene.clearEnemyHeldItemModifiers(); - game.scene.clearEnemyModifiers(); + game.scene.clearEnemyItems(); // Mock stats by replacing entries in copy with desired values for specific stats const stats = { @@ -219,7 +219,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Get rid of any modifiers that may alter power game.scene.clearEnemyHeldItemModifiers(); - game.scene.clearEnemyModifiers(); + game.scene.clearEnemyItems(); // Mock stats by replacing entries in copy with desired values for specific stats const stats = { diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 85193d1ec72..213fea75081 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -20,7 +20,7 @@ import { UiMode } from "#enums/ui-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter"; import { TrainerType } from "#enums/trainer-type"; import { AbilityId } from "#enums/ability-id"; @@ -296,10 +296,10 @@ describe("Clowning Around - Mystery Encounter", () => { const leadItemsAfter = scene.getPlayerParty()[0].getHeldItems(); const ultraCountAfter = leadItemsAfter - .filter(m => m.type.tier === ModifierTier.ULTRA) + .filter(m => m.type.tier === RewardTier.ULTRA) .reduce((a, b) => a + b.stackCount, 0); const rogueCountAfter = leadItemsAfter - .filter(m => m.type.tier === ModifierTier.ROGUE) + .filter(m => m.type.tier === RewardTier.ROGUE) .reduce((a, b) => a + b.stackCount, 0); expect(ultraCountAfter).toBe(13); expect(rogueCountAfter).toBe(7); @@ -391,5 +391,5 @@ async function addItemToPokemon( const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier; itemMod.stackCount = stackCount; scene.addModifier(itemMod, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); } diff --git a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 3ef8431cc2c..c88e1677bcf 100644 --- a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -134,7 +134,7 @@ describe("Delibird-y - Mystery Encounter", () => { const amuletCoin = generateModifierType(modifierTypes.AMULET_COIN)!.newModifier() as MoneyMultiplierModifier; amuletCoin.stackCount = 5; scene.addModifier(amuletCoin, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 1); @@ -204,7 +204,7 @@ describe("Delibird-y - Mystery Encounter", () => { const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; sitrusMod.stackCount = 2; scene.addModifier(sitrusMod, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); @@ -225,7 +225,7 @@ describe("Delibird-y - Mystery Encounter", () => { const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); @@ -251,7 +251,7 @@ describe("Delibird-y - Mystery Encounter", () => { const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; sitrusMod.stackCount = 2; scene.addModifier(sitrusMod, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); @@ -280,7 +280,7 @@ describe("Delibird-y - Mystery Encounter", () => { const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); @@ -303,7 +303,7 @@ describe("Delibird-y - Mystery Encounter", () => { const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]); scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await game.phaseInterceptor.to(MysteryEncounterPhase, false); @@ -332,7 +332,7 @@ describe("Delibird-y - Mystery Encounter", () => { const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); @@ -366,7 +366,7 @@ describe("Delibird-y - Mystery Encounter", () => { const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 2; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); @@ -387,7 +387,7 @@ describe("Delibird-y - Mystery Encounter", () => { const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); @@ -413,7 +413,7 @@ describe("Delibird-y - Mystery Encounter", () => { const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); @@ -436,7 +436,7 @@ describe("Delibird-y - Mystery Encounter", () => { const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const modifier = revSeed.newModifier(scene.getPlayerParty()[0]); scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await game.phaseInterceptor.to(MysteryEncounterPhase, false); @@ -466,7 +466,7 @@ describe("Delibird-y - Mystery Encounter", () => { const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); diff --git a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index bb598f4ae6e..5dc3ba90bb0 100644 --- a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -17,7 +17,7 @@ import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/myst import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { UiMode } from "#enums/ui-mode"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import * as Utils from "#app/utils/common"; const namespace = "mysteryEncounters/globalTradeSystem"; @@ -223,7 +223,7 @@ describe("Global Trade System - Mystery Encounter", () => { const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 2; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); @@ -234,7 +234,7 @@ describe("Global Trade System - Mystery Encounter", () => { h => h instanceof ModifierSelectUiHandler, ) as ModifierSelectUiHandler; expect(modifierSelectHandler.options.length).toEqual(1); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier).toBe(ModifierTier.MASTER); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier).toBe(RewardTier.MASTER); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); expect(soulDewAfter?.stackCount).toBe(1); }); @@ -250,7 +250,7 @@ describe("Global Trade System - Mystery Encounter", () => { const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); diff --git a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index 478648d88a7..0b9d3f2a5f3 100644 --- a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -14,7 +14,7 @@ import { UiMode } from "#enums/ui-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { MysteriousChallengersEncounter } from "#app/data/mystery-encounters/encounters/mysterious-challengers-encounter"; import { TrainerConfig } from "#app/data/trainers/trainer-config"; import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate"; @@ -218,19 +218,19 @@ describe("Mysterious Challengers - Mystery Encounter", () => { expect( modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ULTRA); + ).toBe(RewardTier.ULTRA); expect( modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ULTRA); + ).toBe(RewardTier.ULTRA); expect( modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.GREAT); + ).toBe(RewardTier.GREAT); expect( modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.GREAT); + ).toBe(RewardTier.GREAT); }); }); @@ -275,19 +275,19 @@ describe("Mysterious Challengers - Mystery Encounter", () => { expect( modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ROGUE); + ).toBe(RewardTier.ROGUE); expect( modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ROGUE); + ).toBe(RewardTier.ROGUE); expect( modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ULTRA); + ).toBe(RewardTier.ULTRA); expect( modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.GREAT); + ).toBe(RewardTier.GREAT); }); }); }); diff --git a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index 9ab5f16d1b9..7626e4c9660 100644 --- a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -14,7 +14,7 @@ import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { HealShopCostModifier, HitHealModifier, TurnHealModifier } from "#app/modifier/modifier"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import { CommandPhase } from "#app/phases/command-phase"; @@ -254,19 +254,19 @@ describe("Trash to Treasure - Mystery Encounter", () => { expect( modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + ).toEqual(RewardTier.ROGUE); expect( modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + ).toEqual(RewardTier.ROGUE); expect( modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ULTRA); + ).toEqual(RewardTier.ULTRA); expect( modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.GREAT); + ).toEqual(RewardTier.GREAT); }); }); }); diff --git a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index ec64a17d291..2fef4fa4cd3 100644 --- a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -188,7 +188,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { scene.modifiers.forEach(mod => { scene.removeModifier(mod); }); - await scene.updateModifiers(true); + await scene.updateItems(true); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.phaseManager.getCurrentPhase(); @@ -221,7 +221,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier; ganlonMod.stackCount = 3; scene.addModifier(ganlonMod, true, false, false, true); - await scene.updateModifiers(true); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 2); diff --git a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index 475d5cc3c6e..3690a64cab4 100644 --- a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -19,7 +19,7 @@ import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/wei import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { CommandPhase } from "#app/phases/command-phase"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; const namespace = "mysteryEncounters/weirdDream"; const defaultParty = [SpeciesId.MAGBY, SpeciesId.HAUNTER, SpeciesId.ABRA]; @@ -207,27 +207,27 @@ describe("Weird Dream - Mystery Encounter", () => { expect( modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + ).toEqual(RewardTier.ROGUE); expect( modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + ).toEqual(RewardTier.ROGUE); expect( modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ULTRA); + ).toEqual(RewardTier.ULTRA); expect( modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ULTRA); + ).toEqual(RewardTier.ULTRA); expect( modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.GREAT); + ).toEqual(RewardTier.GREAT); expect( modifierSelectHandler.options[5].modifierTypeOption.type.tier - modifierSelectHandler.options[5].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.GREAT); + ).toEqual(RewardTier.GREAT); }); }); diff --git a/test/phases/select-modifier-phase.test.ts b/test/phases/select-modifier-phase.test.ts index b6c3089e236..0a4d80c7ed7 100644 --- a/test/phases/select-modifier-phase.test.ts +++ b/test/phases/select-modifier-phase.test.ts @@ -1,7 +1,7 @@ import type BattleScene from "#app/battle-scene"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { PlayerPokemon } from "#app/field/pokemon"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import type { CustomModifierSettings } from "#app/modifier/modifier-type"; import { ModifierTypeOption } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; @@ -129,7 +129,7 @@ describe("SelectModifierPhase", () => { h => h instanceof ModifierSelectUiHandler, ) as ModifierSelectUiHandler; expect(modifierSelectHandler.options.length).toEqual(3); - const firstRollTiers: ModifierTier[] = modifierSelectHandler.options.map(o => o.modifierTypeOption.type.tier); + const firstRollTiers: RewardTier[] = modifierSelectHandler.options.map(o => o.modifierTypeOption.type.tier); // TODO: nagivate ui to reroll with lock capsule enabled @@ -184,11 +184,11 @@ describe("SelectModifierPhase", () => { scene.money = 1000000; const customModifiers: CustomModifierSettings = { guaranteedModifierTiers: [ - ModifierTier.COMMON, - ModifierTier.GREAT, - ModifierTier.ULTRA, - ModifierTier.ROGUE, - ModifierTier.MASTER, + RewardTier.COMMON, + RewardTier.GREAT, + RewardTier.ULTRA, + RewardTier.ROGUE, + RewardTier.MASTER, ], }; const pokemon = new PlayerPokemon(getPokemonSpecies(SpeciesId.BULBASAUR), 10, undefined, 0, undefined, true, 2); @@ -212,23 +212,23 @@ describe("SelectModifierPhase", () => { expect( modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.COMMON); + ).toEqual(RewardTier.COMMON); expect( modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.GREAT); + ).toEqual(RewardTier.GREAT); expect( modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ULTRA); + ).toEqual(RewardTier.ULTRA); expect( modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + ).toEqual(RewardTier.ROGUE); expect( modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.MASTER); + ).toEqual(RewardTier.MASTER); }); it("should generate custom modifiers and modifier tiers together", async () => { @@ -236,7 +236,7 @@ describe("SelectModifierPhase", () => { scene.money = 1000000; const customModifiers: CustomModifierSettings = { guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.TM_COMMON], - guaranteedModifierTiers: [ModifierTier.MASTER, ModifierTier.MASTER], + guaranteedModifierTiers: [RewardTier.MASTER, RewardTier.MASTER], }; const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); scene.phaseManager.unshiftPhase(selectModifierPhase); @@ -250,8 +250,8 @@ describe("SelectModifierPhase", () => { expect(modifierSelectHandler.options.length).toEqual(4); expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("TM_COMMON"); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); - expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier).toEqual(RewardTier.MASTER); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier).toEqual(RewardTier.MASTER); }); it("should fill remaining modifiers if fillRemaining is true with custom modifiers", async () => { @@ -259,7 +259,7 @@ describe("SelectModifierPhase", () => { scene.money = 1000000; const customModifiers: CustomModifierSettings = { guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM], - guaranteedModifierTiers: [ModifierTier.MASTER], + guaranteedModifierTiers: [RewardTier.MASTER], fillRemaining: true, }; const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); @@ -273,6 +273,6 @@ describe("SelectModifierPhase", () => { ) as ModifierSelectUiHandler; expect(modifierSelectHandler.options.length).toEqual(3); expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier).toEqual(RewardTier.MASTER); }); }); diff --git a/test/testUtils/gameManager.ts b/test/testUtils/gameManager.ts index 91f9096f3d4..e8ce3e430b7 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -542,8 +542,7 @@ export default class GameManager { * Removes all held items from enemy pokemon. */ removeEnemyHeldItems(): void { - this.scene.clearEnemyHeldItemModifiers(); - this.scene.clearEnemyModifiers(); + this.scene.clearEnemyItems(); console.log("Enemy held items removed"); } } diff --git a/test/testUtils/helpers/overridesHelper.ts b/test/testUtils/helpers/overridesHelper.ts index 8bf68489479..69b319eb40a 100644 --- a/test/testUtils/helpers/overridesHelper.ts +++ b/test/testUtils/helpers/overridesHelper.ts @@ -20,6 +20,8 @@ import type { Unlockables } from "#enums/unlockables"; import { WeatherType } from "#enums/weather-type"; import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper"; import { expect, vi } from "vitest"; +import type { HeldItemConfiguration } from "#app/items/held-item-data-types"; +import type { TrainerItemConfiguration } from "#app/items/trainer-item-data-types"; /** * Helper to handle overrides in tests @@ -115,9 +117,20 @@ export class OverridesHelper extends GameManagerHelper { * @param items - The items to hold * @returns `this` */ - public startingHeldItems(items: ModifierOverride[]): this { - vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); - this.log("Player Pokemon starting held items set to:", items); + public startingHeldItems(itemConfiguration: HeldItemConfiguration): this { + vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(itemConfiguration); + this.log("Player Pokemon starting held items set to:", itemConfiguration); + return this; + } + + /** + * Override the player's starting trainer items + * @param items - The items to have + * @returns `this` + */ + public startingTrainerItems(itemConfiguration: TrainerItemConfiguration): this { + vi.spyOn(Overrides, "STARTING_TRAINER_ITEMS_OVERRIDE", "get").mockReturnValue(itemConfiguration); + this.log("Player starting trainer items set to:", itemConfiguration); return this; } @@ -167,17 +180,6 @@ export class OverridesHelper extends GameManagerHelper { return this; } - /** - * Override the player's starting modifiers - * @param modifiers - The modifiers to set - * @returns `this` - */ - public startingModifier(modifiers: ModifierOverride[]): this { - vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers); - this.log(`Player starting modifiers set to: ${modifiers}`); - return this; - } - /** * Override the player pokemon's {@linkcode AbilityId | ability}. * @param ability - The {@linkcode AbilityId | ability} to set @@ -509,9 +511,20 @@ export class OverridesHelper extends GameManagerHelper { * @param items the items to hold * @returns `this` */ - public enemyHeldItems(items: ModifierOverride[]): this { - vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); - this.log("Enemy Pokemon held items set to:", items); + public enemyHeldItems(itemConfiguration: HeldItemConfiguration): this { + vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(itemConfiguration); + this.log("Enemy Pokemon held items set to:", itemConfiguration); + return this; + } + + /** + * Override the enemy's trainer items + * @param items - The items to have + * @returns `this` + */ + public enemyTrainerItems(itemConfiguration: TrainerItemConfiguration): this { + vi.spyOn(Overrides, "OPP_TRAINER_ITEMS_OVERRIDE", "get").mockReturnValue(itemConfiguration); + this.log("Enemy trainer items set to:", itemConfiguration); return this; } diff --git a/test/testUtils/helpers/reloadHelper.ts b/test/testUtils/helpers/reloadHelper.ts index 4f9d6c810f8..1715bee5c1c 100644 --- a/test/testUtils/helpers/reloadHelper.ts +++ b/test/testUtils/helpers/reloadHelper.ts @@ -46,16 +46,6 @@ export class ReloadHelper extends GameManagerHelper { scene.phaseManager.unshiftPhase(titlePhase); this.game.endPhase(); // End the currently ongoing battle - // remove all persistent mods before loading - // TODO: Look into why these aren't removed before load - if (this.game.scene.modifiers.length) { - console.log( - "Removing %d modifiers from scene on load...", - this.game.scene.modifiers.length, - this.game.scene.modifiers, - ); - this.game.scene.modifiers = []; - } titlePhase.loadSaveSlot(-1); // Load the desired session data this.game.phaseInterceptor.shift(); // Loading the save slot also ended TitlePhase, clean it up diff --git a/test/testUtils/testFileInitialization.ts b/test/testUtils/testFileInitialization.ts index 578747b0529..ed50f6a3683 100644 --- a/test/testUtils/testFileInitialization.ts +++ b/test/testUtils/testFileInitialization.ts @@ -24,6 +24,10 @@ import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import { manageListeners } from "./listenersManager"; import { initI18n } from "#app/plugins/i18n"; import { initModifierTypes } from "#app/modifier/modifier-type"; +import { initHeldItems } from "#app/items/all-held-items"; +import { initHeldItemPools } from "#app/items/init-held-item-pools"; +import { initTrainerItems } from "#app/items/all-trainer-items"; +import { initTrainerItemPools } from "#app/items/init-trainer-item-pools"; let wasInitialized = false; /** @@ -90,6 +94,10 @@ export function initTestFile() { if (!wasInitialized) { wasInitialized = true; initI18n(); + initHeldItems(); + initHeldItemPools(); + initTrainerItems(); + initTrainerItemPools(); initModifierTypes(); initModifierPools(); initVouchers();