diff --git a/src/battle-scene.ts b/src/battle-scene.ts index b802466ee19..f196fc930a9 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -20,7 +20,7 @@ import { type Constructor, } from "#app/utils/common"; import { deepMergeSpriteData } from "#app/utils/data"; -import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier"; +import type { Modifier, ModifierPredicate } from "./modifier/modifier"; import { ConsumableModifier, ConsumablePokemonModifier, @@ -29,14 +29,9 @@ import { ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, - ModifierBar, MultipleParticipantExpBonusModifier, PersistentModifier, - PokemonExpBoosterModifier, - PokemonFormChangeItemModifier, - PokemonHeldItemModifier, PokemonHpRestoreModifier, - PokemonIncrementingStatModifier, RememberMoveModifier, } from "./modifier/modifier"; import { PokeballType } from "#enums/pokeball"; @@ -56,16 +51,12 @@ 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 { 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, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs } from "./data/abilities/apply-ab-attrs"; import { allAbilities } from "./data/data-lists"; @@ -88,7 +79,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"; @@ -101,6 +92,7 @@ import type { SpeciesFormChangeTrigger } from "./data/pokemon-forms/form-change- import { pokemonFormChanges } from "#app/data/pokemon-forms"; import { SpeciesFormChangeTimeOfDayTrigger } from "./data/pokemon-forms/form-change-triggers"; import { SpeciesFormChangeManualTrigger } from "./data/pokemon-forms/form-change-triggers"; +import { SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { FormChangeItem } from "#enums/form-change-item"; import { getTypeRgb } from "#app/data/type"; import { PokemonType } from "#enums/pokemon-type"; @@ -148,7 +140,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"; @@ -160,7 +151,13 @@ 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 { ModifierBar } from "./modifier/modifier-bar"; +import { allHeldItems, applyHeldItems } from "./items/all-held-items"; +import { 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"; const DEBUG_RNG = false; @@ -909,6 +906,7 @@ export default class BattleScene extends SceneBase { variant?: Variant, ivs?: number[], nature?: Nature, + heldItemConfig?: HeldItemConfiguration, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void, ): PlayerPokemon { @@ -928,6 +926,9 @@ export default class BattleScene extends SceneBase { postProcess(pokemon); } pokemon.init(); + if (heldItemConfig) { + assignItemsFromConfiguration(heldItemConfig, pokemon); + } return pokemon; } @@ -937,6 +938,7 @@ export default class BattleScene extends SceneBase { trainerSlot: TrainerSlot, boss = false, shinyLock = false, + heldItemConfig?: HeldItemConfiguration, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void, ): EnemyPokemon { @@ -978,6 +980,9 @@ export default class BattleScene extends SceneBase { } pokemon.init(); + if (heldItemConfig) { + assignItemsFromConfiguration(heldItemConfig, pokemon); + } return pokemon; } @@ -2085,9 +2090,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); } @@ -2618,15 +2621,8 @@ 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); } @@ -2644,12 +2640,8 @@ export default class BattleScene extends SceneBase { return this.addModifier(defaultModifierType.newModifier(), ignoreUpdate, playSound, false, instant); } - for (const rm of modifiersToRemove) { - this.removeModifier(rm); - } - if (!ignoreUpdate && !virtual) { - this.updateModifiers(true, instant); + this.updateModifiers(true); } } else if (modifier instanceof ConsumableModifier) { if (playSound && !this.sound.get(soundName)) { @@ -2693,35 +2685,51 @@ 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(); - } - }); + addEnemyModifier(modifier: PersistentModifier, ignoreUpdate?: boolean) { + (modifier as PersistentModifier).add(this.enemyModifiers, false); + if (!ignoreUpdate) { + this.updateModifiers(false); + } + } + + addHeldItem(heldItemId: HeldItemId, pokemon: Pokemon, amount = 1, playSound?: boolean, ignoreUpdate?: boolean) { + pokemon.heldItemManager.add(heldItemId, amount); + if (!ignoreUpdate) { + this.updateModifiers(pokemon.isPlayer()); + } + const soundName = allHeldItems[heldItemId].soundName; + if (playSound && !this.sound.get(soundName)) { + this.playSound(soundName); + } + + if (pokemon.isPlayer()) { + this.validateAchvs(HeldItemAchv, pokemon); + } + } + + addFormChangeItem(itemId: FormChangeItem, pokemon: Pokemon, ignoreUpdate?: boolean) { + if (pokemon.heldItemManager.hasFormChangeItem(itemId)) { + return; + } + + pokemon.heldItemManager.addFormChangeItem(itemId); + + this.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger); + + pokemon.heldItemManager.toggleActive(itemId); + + if (!ignoreUpdate) { + this.updateModifiers(false); + } } /** - * 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` @@ -2730,16 +2738,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()) { @@ -2750,65 +2757,39 @@ 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) { + applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false); } - const removeOld = itemModifier.stackCount === 0; - - 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) { - applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false); - } - return true; - } - this.addEnemyModifier(newItemModifier, ignoreUpdate, instant); - if (source && itemLost) { - applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false); - } - return true; - } - return false; - }; - if (source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) { - this.updateModifiers(source.isPlayer(), instant); - addModifier(); - } else { - addModifier(); - } - return true; + if (source.isPlayer() !== target.isPlayer() && !ignoreUpdate) { + this.updateModifiers(source.isPlayer()); } - return false; + + const soundName = allHeldItems[heldItemId].soundName; + if (playSound && !this.sound.get(soundName)) { + this.playSound(soundName); + } + + return true; } - canTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, transferQuantity = 1): boolean { - const mod = itemModifier.clone() as PokemonHeldItemModifier; - const source = mod.pokemonId ? mod.getPokemon() : null; + canTransferHeldItem(heldItemId: HeldItemId, source: Pokemon, target: Pokemon, transferQuantity = 1): boolean { const cancelled = new BooleanHolder(false); if (source && source.isPlayer() !== target.isPlayer()) { @@ -2819,43 +2800,19 @@ export default class BattleScene extends SceneBase { return false; } - const matchingModifier = this.findModifier( - m => m instanceof PokemonHeldItemModifier && m.matchType(mod) && 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, mod.stackCount, maxStackCount - matchingModifier.stackCount); - mod.stackCount -= countTaken; - } else { - const countTaken = Math.min(transferQuantity, mod.stackCount); - mod.stackCount -= countTaken; + const maxStackCount = allHeldItems[heldItemId].getMaxStackCount(); + if (matchingItemStack >= maxStackCount) { + return false; } + const countTaken = Math.min(transferQuantity, itemStack, maxStackCount - matchingItemStack); - const removeOld = mod.stackCount === 0; - - return !removeOld || !source || this.hasModifier(itemModifier, !source.isPlayer()); + return countTaken > 0; } - 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 { + generateEnemyModifiers(heldItemConfigs?: HeldItemConfiguration[]): Promise { return new Promise(resolve => { if (this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { return resolve(); @@ -2872,24 +2829,13 @@ export default class BattleScene extends SceneBase { if (this.currentBattle.trainer) { const modifiers = this.currentBattle.trainer.genModifiers(party); for (const modifier of modifiers) { - this.addEnemyModifier(modifier, true, true); + this.addEnemyModifier(modifier, true); } } 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() || @@ -2910,13 +2856,13 @@ 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; }); @@ -2937,50 +2883,21 @@ export default class BattleScene extends SceneBase { 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.updateUIPositions(); - } - setModifiersVisible(visible: boolean) { [this.modifierBar, this.enemyModifierBar].map(m => m.setVisible(visible)); } // TODO: Document this - updateModifiers(player = true, instant?: boolean): void { + updateModifiers(player = true): 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; } } + // Removing modifiers with a stack count of 0, if they somehow got to this point const modifiersClone = modifiers.slice(0); for (const modifier of modifiersClone) { if (!modifier.getStackCount()) { @@ -2988,14 +2905,17 @@ export default class BattleScene extends SceneBase { } } - this.updatePartyForModifiers(player ? this.getPlayerParty() : this.getEnemyParty(), instant); - (player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers); + this.updateParty(player ? this.getPlayerParty() : this.getEnemyParty(), true); + + const pokemonA = player ? this.getPlayerParty()[0] : this.getEnemyParty()[0]; + + (player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers, pokemonA); 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 => { @@ -3024,12 +2944,6 @@ export default class BattleScene extends SceneBase { 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; } @@ -3172,22 +3086,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) { @@ -3317,11 +3226,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); @@ -3385,10 +3290,10 @@ 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.updateModifiers(true); partyMember.updateInfo(); } } @@ -3415,7 +3320,7 @@ export default class BattleScene extends SceneBase { expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; } const pokemonExp = new NumberHolder(expValue * expMultiplier); - this.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); + applyHeldItems(ITEM_EFFECT.EXP_BOOSTER, { pokemon: partyMember, expAmount: pokemonExp }); partyMemberExp.push(Math.floor(pokemonExp.value)); } diff --git a/src/battle.ts b/src/battle.ts index 245705f4801..313870b0c37 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -13,7 +13,7 @@ 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 { MoneyMultiplierModifier } from "./modifier/modifier"; import type { PokeballType } from "#enums/pokeball"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { SpeciesFormKey } from "#enums/species-form-key"; @@ -30,10 +30,11 @@ 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"; export interface TurnCommand { @@ -71,7 +72,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); @@ -170,19 +171,7 @@ 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-ignore - this is awful to fix/change - ret.pokemonId = null; - return ret; - }), - ); + this.postBattleLoot.push(...enemyPokemon.getHeldItems()); } pickUpScatteredMoney(): void { @@ -604,7 +593,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() @@ -636,7 +625,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() @@ -709,7 +698,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() @@ -772,11 +761,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, }), @@ -791,11 +780,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, }), @@ -818,12 +807,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, }), @@ -922,12 +911,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 120d1d413c4..f699a03b793 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/items/all-held-items"; +import { berryTypeToHeldItem } from "#app/items/held-items/berry"; import type { Localizable } from "#app/@types/locales"; import { applyAbAttrs } from "./apply-ab-attrs"; @@ -2759,7 +2760,7 @@ export class GorillaTacticsAbAttr extends ExecutedMoveAbAttr { export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { private stealCondition: PokemonAttackCondition | null; - private stolenItem?: PokemonHeldItemModifier; + private stolenItem?: HeldItemId; constructor(stealCondition?: PokemonAttackCondition) { super(); @@ -2782,11 +2783,11 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { hitResult < HitResult.NO_EFFECT && (!this.stealCondition || this.stealCondition(pokemon, defender, move)) ) { - const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferable); + const heldItems = defender.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, defender, pokemon)) { return true; } } @@ -2804,28 +2805,21 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { _hitResult: HitResult, _args: any[], ): void { - const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferable); + const heldItems = defender.heldItemManager.getTransferableHeldItems(); if (!this.stolenItem) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; } - if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { + if (globalScene.tryTransferHeldItem(this.stolenItem, defender, pokemon, false)) { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postAttackStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), defenderName: defender.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 { @@ -2946,7 +2940,7 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr { export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { private condition?: PokemonDefendCondition; - private stolenItem?: PokemonHeldItemModifier; + private stolenItem?: HeldItemId; constructor(condition?: PokemonDefendCondition) { super(); @@ -2964,10 +2958,10 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { _args: any[], ): boolean { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move))) { - const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferable); + const heldItems = attacker.heldItemManager.getTransferableHeldItems(); if (heldItems.length) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; - if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { + if (globalScene.canTransferHeldItem(this.stolenItem, attacker, pokemon)) { return true; } } @@ -2984,28 +2978,21 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { _hitResult: HitResult, _args: any[], ): void { - const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferable); + const heldItems = attacker.heldItemManager.getTransferableHeldItems(); if (!this.stolenItem) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; } - if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { + if (globalScene.tryTransferHeldItem(this.stolenItem, attacker, pokemon, false)) { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postDefendStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), attackerName: attacker.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[]; - } } /** @@ -5643,10 +5630,14 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): 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].berryType), ); this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(bt => !cappedBerries.has(bt)); @@ -5676,30 +5667,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; - - if (berryModifier) { - berryModifier.stackCount++; - } else { - const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1); - if (pokemon.isPlayer()) { - globalScene.addModifier(newBerry); - } else { - globalScene.addEnemyModifier(newBerry); - } - } + pokemon.heldItemManager.add(chosenBerry); globalScene.updateModifiers(pokemon.isPlayer()); globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - berryName: chosenBerry.name, + berryName: allHeldItems[chosenBerry].name, }), ); return true; @@ -5748,8 +5724,7 @@ export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr { // 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 @@ -6417,13 +6392,13 @@ export class PostBattleAbAttr extends AbAttr { } export class PostBattleLootAbAttr extends PostBattleAbAttr { - private randItem?: PokemonHeldItemModifier; + private randItem?: HeldItemId; override canApplyPostBattle(pokemon: Pokemon, _passive: boolean, simulated: boolean, args: any[]): boolean { const postBattleLoot = globalScene.currentBattle.postBattleLoot; if (!simulated && postBattleLoot.length && args[0]) { this.randItem = randSeedItem(postBattleLoot); - return globalScene.canTransferHeldItemModifier(this.randItem, pokemon, 1); + return pokemon.heldItemManager.getStack(this.randItem) < allHeldItems[this.randItem].maxStackCount; } return false; } @@ -6437,12 +6412,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, }), ); } @@ -7457,8 +7432,6 @@ class ForceSwitchOutHelper { } if (!allyPokemon?.isActive(true)) { - globalScene.clearEnemyHeldItemModifiers(); - if (switchOutTarget.hp) { globalScene.phaseManager.pushNew("BattleEndPhase", false); @@ -7542,11 +7515,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; } /** diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 5dda1912e44..318a841c906 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, @@ -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/moves/move.ts b/src/data/moves/move.ts index f94c59bb463..41e26e73170 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -41,11 +41,6 @@ import { } from "../abilities/apply-ab-attrs"; import { allAbilities, allMoves } from "../data-lists"; import { - AttackTypeBoosterModifier, - BerryModifier, - PokemonHeldItemModifier, - PokemonMoveAccuracyBoosterModifier, - PokemonMultiHitModifier, PreserveBerryModifier, } from "../../modifier/modifier"; import type { BattlerIndex } from "#enums/battler-index"; @@ -89,6 +84,10 @@ 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 { allHeldItems, applyHeldItems } from "#app/items/all-held-items"; +import { ITEM_EFFECT } from "#app/items/held-item"; +import { berryTypeToHeldItem } from "#app/items/held-items/berry"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import { applyMoveAttrs } from "./apply-attrs"; import { frenzyMissFunc, getMoveTargets } from "./move-utils"; @@ -771,7 +770,7 @@ export default abstract class Move implements Localizable { const isOhko = this.hasAttr("OneHitKOAccuracyAttr"); if (!isOhko) { - globalScene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); + applyHeldItems(ITEM_EFFECT.ACCURACY_BOOSTER, { pokemon: user, moveAccuracy: moveAccuracy }); } if (globalScene.arena.weather?.weatherType === WeatherType.FOG) { @@ -808,7 +807,7 @@ export default abstract class Move implements Localizable { applyPreAttackAbAttrs("MoveTypeChangeAbAttr", source, target, this, true, typeChangeHolder, 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; } @@ -844,7 +843,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(ITEM_EFFECT.ATTACK_TYPE_BOOST, { + pokemon: source, + moveType: typeChangeHolder.value, + movePower: power, + }); } if (source.getTag(HelpingHandTag)) { @@ -907,7 +910,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`) @@ -1558,7 +1561,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); @@ -2612,35 +2615,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; } } @@ -2686,10 +2687,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) { @@ -2703,26 +2704,23 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { globalScene.updateModifiers(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; } } @@ -2731,7 +2729,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); } @@ -2771,9 +2769,8 @@ 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) { @@ -2794,10 +2791,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].berryType)(consumer); applyPostItemLostAbAttrs("PostItemLostAbAttr", berryOwner, false); applyAbAttrs("HealFromBerryUseAbAttr", consumer, new BooleanHolder(false)); - consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest); + consumer.recordEatenBerry(allHeldItems[this.chosenBerry].berryType, updateHarvest); } } @@ -2836,7 +2833,7 @@ export class StealEatBerryAttr extends EatBerryAttr { // pick a random berry and eat it this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; applyPostItemLostAbAttrs("PostItemLostAbAttr", target, false); - 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); @@ -6415,9 +6412,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); @@ -8008,14 +8002,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; }; @@ -9310,7 +9304,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. @@ -10032,7 +10026,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), @@ -10781,7 +10775,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..4008a6f36f7 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/items/all-held-items"; +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,9 +162,9 @@ 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 => { + map.pokemon.heldItemManager.remove(map.item); }); globalScene.updateModifiers(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,19 +230,16 @@ 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); } } }); 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..6937945eeca 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 { 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/items/all-held-items"; /** 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; @@ -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.updateModifiers(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 9bdaa603540..eb8fdc4a85e 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..87dca8b9bf7 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -14,20 +14,16 @@ import { 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,24 +33,34 @@ 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/items/all-held-items"; /** 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) { @@ -169,16 +175,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { // 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 +202,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,11 +235,11 @@ 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, @@ -252,18 +247,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { // 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(); @@ -274,25 +258,14 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { // 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 +288,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; }, @@ -360,16 +329,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { // 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..4a68066748d 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/items/all-held-items"; 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/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 6bbc1a68772..433c4d423f2 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,7 @@ 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 { HiddenAbilityRateBoosterModifier, ShinyRateBoosterModifier } 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 +46,10 @@ 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/items/all-held-items"; +import { RewardTier } from "#enums/reward-tier"; +import { getHeldItemTier } from "#app/items/held-item-tiers"; /** 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); @@ -336,9 +329,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 +354,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 +384,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 +403,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 +415,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 +443,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil fillRemaining: false, }); - chosenPokemon.loseHeldItem(modifier, false); - await globalScene.updateModifiers(true, true); + chosenPokemon.heldItemManager.remove(heldItemId); + await globalScene.updateModifiers(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 010358ea3b2..4e63291583f 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/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 967c105c740..02eca80cf31 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/items/all-held-items"; /** 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 4169fd6d7c5..b2bc2321524 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/custom-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-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 37d16075543..e5aafbfdeb9 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"; @@ -23,11 +21,11 @@ import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encou 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/custom-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 +93,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) => { 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 eba8a6ba00e..08ffc6eb492 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 { applyPostBattleInitAbAttrs } 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 04e64083602..688e62d8f9c 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,12 +149,7 @@ 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); - } + // Make held items show up again globalScene.updateModifiers(true); queueEncounterMessage(`${namespace}:option.1.finished`); }; @@ -218,8 +210,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 = () => { @@ -228,12 +219,7 @@ 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); - } + // Make held items show up again globalScene.updateModifiers(true); }; @@ -309,8 +295,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); @@ -341,12 +326,7 @@ 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); - } + // Make held items show up again globalScene.updateModifiers(true); }; @@ -374,18 +354,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 { @@ -397,12 +371,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..c5770ece3f5 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -1,5 +1,6 @@ import type { EnemyPartyConfig, EnemyPokemonConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { + assignItemToFirstFreePokemon, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, @@ -7,7 +8,6 @@ import { 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"; @@ -17,11 +17,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 +27,8 @@ 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/items/all-held-items"; /** 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 = { @@ -200,7 +172,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 +196,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 +216,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..36ed8c9ea4d 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]; + randBerry.pokemon.heldItemManager.remove(randBerry.item); + stolenBerryMap.push(randBerry); + berryMap.splice(index, 1); } - await globalScene.updateModifiers(true, true); + await globalScene.updateModifiers(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 1ba756c7f5d..e0d6989b498 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,9 @@ 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 { HiddenAbilityRateBoosterModifier } 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 { @@ -35,15 +32,17 @@ import { import { getLevelTotalExp } from "#app/data/exp"; import { Stat } from "#enums/stat"; 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"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/weirdDream"; @@ -265,24 +264,20 @@ 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)) { const stats = getOldGateauBoostedStats(newPokemon); - newPokemonHeldItemConfigs.push({ - modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [ - OLD_GATEAU_STATS_UP, - stats, - ]) as PokemonHeldItemModifierType, - stackCount: 1, - isTransferable: false, + const gateauItem = { + id: HeldItemId.OLD_GATEAU, + stack: 1, + data: { statModifier: OLD_GATEAU_STATS_UP, stats: stats }, + } as HeldItemSpecs; + newPokemonHeldItemConfig.push({ + entry: gateauItem, + count: 1, }); } @@ -291,7 +286,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); @@ -323,12 +318,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, }, @@ -372,7 +367,7 @@ interface PokemonTransformation { previousPokemon: PlayerPokemon; newSpecies: PokemonSpecies; newPokemon: PlayerPokemon; - heldItems: PokemonHeldItemModifier[]; + heldItems: HeldItemConfiguration; } function getTeamTransformations(): PokemonTransformation[] { @@ -397,9 +392,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]; @@ -455,22 +448,22 @@ 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 stats = getOldGateauBoostedStats(newPokemon); - const modType = modifierTypes - .MYSTERY_ENCOUNTER_OLD_GATEAU() - .generateType(globalScene.getPlayerParty(), [OLD_GATEAU_STATS_UP, stats]) - ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); - const modifier = modType?.newModifier(newPokemon); - if (modifier) { - globalScene.addModifier(modifier, false, false, false, true); - } + const gateauItem = { + id: HeldItemId.OLD_GATEAU, + stack: 1, + data: { statModifier: OLD_GATEAU_STATS_UP, stats: stats }, + } as HeldItemSpecs; + heldItemConfiguration.push({ + entry: gateauItem, + count: 1, + }); } + assignItemsFromConfiguration(heldItemConfiguration, newPokemon); newPokemon.calculateStats(); await newPokemon.updateInfo(); diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 07fd155b2b2..6b26f66f436 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/items/all-held-items"; 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 bb74f11ce60..8d71590daf5 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -44,7 +44,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/custom-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 +52,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/items/all-held-items"; /** * Animates exclamation sprite over trainer's head at start of encounter @@ -102,7 +104,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; @@ -434,8 +436,8 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, ); const customModifierTypes = partyConfig?.pokemonConfigs - ?.filter(config => config?.modifierConfigs) - .map(config => config.modifierConfigs!); + ?.filter(config => config?.heldItemConfig) + .map(config => config.heldItemConfig!); globalScene.generateEnemyModifiers(customModifierTypes); } } @@ -1305,3 +1307,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: berryId, pokemon: pokemon }); + } + }); + }); + 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 f6ac5b0d38b..c01cb3b89f5 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,7 +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"; @@ -37,6 +35,7 @@ import { CustomPokemonData } from "#app/data/custom-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; @@ -387,40 +386,11 @@ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: numb } } -/** - * 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); +export function applyHeldItemWithFallback(pokemon: Pokemon, item: HeldItemId, fallbackItem?: HeldItemId) { + const added = pokemon.heldItemManager.add(item); + if (!added && fallbackItem) { + pokemon.heldItemManager.add(fallbackItem); } - - globalScene.addModifier(modifier, false, false, false, true); } /** @@ -687,20 +657,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) { 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..3d42e546926 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,11 @@ 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; + } + return matchItem.active === this.active; } } diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 6786aa00ef7..5b892eb9015 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -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..823cc614949 --- /dev/null +++ b/src/enums/held-item-id.ts @@ -0,0 +1,145 @@ +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: 0x0907, + OLD_GATEAU: 0x0908, + MACHO_BRACE: 0x0909, + + // 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..99b698e568c 100644 --- a/src/enums/modifier-pool-type.ts +++ b/src/enums/modifier-pool-type.ts @@ -1,7 +1,10 @@ export enum ModifierPoolType { PLAYER, + ENEMY_BUFF, +} + +export enum HeldItemPoolType { WILD, TRAINER, - ENEMY_BUFF, - DAILY_STARTER + DAILY_STARTER, } 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/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 8d7e5037852..ff2d4b2fe82 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -40,7 +40,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 { ITEM_EFFECT } from "#app/items/held-item"; export class Arena { public biomeType: BiomeId; @@ -344,7 +345,7 @@ export class Arena { if (!isNullOrUndefined(user)) { weatherDuration.value = 5; - globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); + applyHeldItems(ITEM_EFFECT.FIELD_EFFECT, { pokemon: user, fieldDuration: weatherDuration }); } this.weather = weather ? new Weather(weather, weatherDuration.value) : null; @@ -431,7 +432,7 @@ export class Arena { if (!isNullOrUndefined(user)) { terrainDuration.value = 5; - globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); + applyHeldItems(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..574cbcdd340 --- /dev/null +++ b/src/field/pokemon-held-item-manager.ts @@ -0,0 +1,190 @@ +import { allHeldItems } from "#app/items/all-held-items"; +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, +} from "#app/items/held-item-data-types"; + +interface FormChangeItemProperties { + active: boolean; +} + +export type FormChangeItemPropertyMap = { + [key in FormChangeItem]?: FormChangeItemProperties; +}; + +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 }); + } + } + return config; + } + + 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; + } + + 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)); + } + + addFormChangeItem(id: FormChangeItem) { + if (!(id in this.formChangeItems)) { + this.formChangeItems[id] = { active: false }; + } + } + + 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 e9cc4f70d70..19ad009e884 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -56,21 +56,9 @@ import { 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"; @@ -151,7 +139,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"; @@ -182,6 +170,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 { 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 { LearnMoveSituation } from "#enums/learn-move-situation"; @@ -303,6 +295,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, @@ -345,6 +339,8 @@ 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 (dataSource) { this.id = dataSource.id; this.hp = dataSource.hp; @@ -1125,14 +1121,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 { @@ -1362,7 +1352,7 @@ 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); + applyHeldItems(ITEM_EFFECT.CRIT_BOOST, { pokemon: source, critStage: critStage }); globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); applyAbAttrs("BonusCritAbAttr", source, null, false, critStage); const critBoostTag = source.getTag(CritBoostTag); @@ -1417,7 +1407,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ): number { const statValue = new NumberHolder(this.getStat(stat, false)); if (!ignoreHeldItems) { - globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); + applyHeldItems(ITEM_EFFECT.STAT_BOOST, { pokemon: this, stat: stat, statValue: statValue }); } // 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(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(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(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(ITEM_EFFECT.BASE_STAT_TOTAL, { pokemon: this, baseStats: baseStats }); // Old Gateau - globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); + applyHeldItems(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(ITEM_EFFECT.BASE_STAT_BOOSTER, { pokemon: this, baseStats: baseStats }); return baseStats; } @@ -2987,11 +2977,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]); } } @@ -3647,14 +3637,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(ITEM_EFFECT.MULTI_HIT, { + pokemon: source, + moveId: move.id, + damageMultiplier: multiLensMultiplier, + }); fixedDamage.value = toDmgValue(fixedDamage.value * multiLensMultiplier.value); return { @@ -3698,14 +3685,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(ITEM_EFFECT.MULTI_HIT, { + pokemon: source, + moveId: move.id, + damageMultiplier: multiStrikeEnhancementMultiplier, + }); if (!ignoreSourceAbility) { applyPreAttackAbAttrs( "AddSecondStrikeAbAttr", @@ -3920,7 +3904,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(ITEM_EFFECT.SURVIVE_CHANCE, { pokemon: this, surviveDamage: surviveDamage }); } if (surviveDamage.value) { damage = this.hp - 1; @@ -4356,7 +4340,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.updateModifiers(this.isPlayer()); Promise.all([this.updateInfo(), globalScene.updateFieldScale()]).then(() => resolve()); }); }); @@ -5441,15 +5425,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) { applyPostItemLostAbAttrs("PostItemLostAbAttr", this, false); } @@ -5640,7 +5622,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(ITEM_EFFECT.FRIENDSHIP_BOOSTER, { pokemon: this, friendship: amount }); const candyFriendshipMultiplier = globalScene.gameMode.isClassic ? timedEventManager.getClassicFriendshipMultiplier() : 1; @@ -5794,9 +5776,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) { @@ -5849,14 +5831,10 @@ 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); } @@ -5903,7 +5881,7 @@ export class PlayerPokemon extends Pokemon { const updateAndResolve = () => { this.loadAssets().then(() => { this.calculateStats(); - globalScene.updateModifiers(true, true); + globalScene.updateModifiers(true); this.updateInfo(true).then(() => resolve()); }); }; @@ -5969,15 +5947,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.updateModifiers(true); globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this); pokemon diff --git a/src/items/all-held-items.ts b/src/items/all-held-items.ts new file mode 100644 index 00000000000..5cbc2a8e344 --- /dev/null +++ b/src/items/all-held-items.ts @@ -0,0 +1,212 @@ +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 { 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 const allHeldItems = {}; + +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)) { + 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, 10, stat) + .unstealable() + .untransferable() + .unsuppressable(); + } + + allHeldItems[HeldItemId.SHUCKLE_JUICE] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE, 1) + .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 = { + [ITEM_EFFECT.ATTACK_TYPE_BOOST]: ATTACK_TYPE_BOOST_PARAMS; + [ITEM_EFFECT.TURN_END_HEAL]: TURN_END_HEAL_PARAMS; + [ITEM_EFFECT.HIT_HEAL]: HIT_HEAL_PARAMS; + [ITEM_EFFECT.RESET_NEGATIVE_STAT_STAGE]: RESET_NEGATIVE_STAT_STAGE_PARAMS; + [ITEM_EFFECT.EXP_BOOSTER]: EXP_BOOST_PARAMS; + [ITEM_EFFECT.BERRY]: BERRY_PARAMS; + [ITEM_EFFECT.BASE_STAT_BOOSTER]: BASE_STAT_BOOSTER_PARAMS; + [ITEM_EFFECT.INSTANT_REVIVE]: INSTANT_REVIVE_PARAMS; + [ITEM_EFFECT.STAT_BOOST]: STAT_BOOST_PARAMS; + [ITEM_EFFECT.CRIT_BOOST]: CRIT_BOOST_PARAMS; + [ITEM_EFFECT.TURN_END_STATUS]: TURN_END_STATUS_PARAMS; + [ITEM_EFFECT.SURVIVE_CHANCE]: SURVIVE_CHANCE_PARAMS; + [ITEM_EFFECT.BYPASS_SPEED_CHANCE]: BYPASS_SPEED_CHANCE_PARAMS; + [ITEM_EFFECT.FLINCH_CHANCE]: FLINCH_CHANCE_PARAMS; + [ITEM_EFFECT.FIELD_EFFECT]: FIELD_EFFECT_PARAMS; + [ITEM_EFFECT.FRIENDSHIP_BOOSTER]: FRIENDSHIP_BOOST_PARAMS; + [ITEM_EFFECT.NATURE_WEIGHT_BOOSTER]: NATURE_WEIGHT_BOOST_PARAMS; + [ITEM_EFFECT.ACCURACY_BOOSTER]: ACCURACY_BOOST_PARAMS; + [ITEM_EFFECT.MULTI_HIT]: MULTI_HIT_PARAMS; + [ITEM_EFFECT.DAMAGE_MONEY_REWARD]: DAMAGE_MONEY_REWARD_PARAMS; + [ITEM_EFFECT.BATON]: BATON_PARAMS; + [ITEM_EFFECT.CONTACT_ITEM_STEAL_CHANCE]: ITEM_STEAL_PARAMS; + [ITEM_EFFECT.TURN_END_ITEM_STEAL]: ITEM_STEAL_PARAMS; + [ITEM_EFFECT.BASE_STAT_TOTAL]: BASE_STAT_TOTAL_PARAMS; + [ITEM_EFFECT.BASE_STAT_FLAT]: BASE_STAT_FLAT_PARAMS; + [ITEM_EFFECT.INCREMENTING_STAT]: INCREMENTING_STAT_PARAMS; + [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/held-item-data-types.ts b/src/items/held-item-data-types.ts new file mode 100644 index 00000000000..099f0f2ee06 --- /dev/null +++ b/src/items/held-item-data-types.ts @@ -0,0 +1,77 @@ +import type Pokemon from "#app/field/pokemon"; +import type { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import type { RewardTier } from "#enums/reward-tier"; +import type { Stat } from "#enums/stat"; + +export interface BASE_STAT_TOTAL_DATA { + statModifier: number; +} + +export interface BASE_STAT_FLAT_DATA { + statModifier: number; + stats: Stat[]; +} + +export type HeldItemExtraData = BASE_STAT_TOTAL_DATA | BASE_STAT_FLAT_DATA; + +export type HeldItemData = { + stack: number; + disabled?: boolean; + unstealable?: boolean; + cooldown?: number; + data?: HeldItemExtraData; +}; + +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; +} + +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 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; + count?: number | (() => number); +}; + +export type HeldItemConfiguration = HeldItemConfigurationEntry[]; + +export type PokemonItemMap = { + item: HeldItemId; + pokemon: Pokemon; +}; diff --git a/src/items/held-item-pool.ts b/src/items/held-item-pool.ts new file mode 100644 index 00000000000..56403891d21 --- /dev/null +++ b/src/items/held-item-pool.ts @@ -0,0 +1,320 @@ +import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import type Pokemon from "#app/field/pokemon"; +import { coerceArray, getEnumValues, randSeedFloat, 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 HeldItemSpecs, + type HeldItemTieredPool, + type HeldItemWeights, + 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, + ); + 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 determineEnemyPoolTier(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 = determineEnemyPoolTier(pool, upgradeCount); + const tierPool = pool[tier]; + + return getNewHeldItemFromPool(tierPool!, pokemon); +} + +function pickWeightedIndex(weights: number[]): number { + const totalWeight = weights.reduce((sum, w) => sum + w, 0); + + if (totalWeight <= 0) { + throw new Error("Total weight must be greater than 0."); + } + + let r = randSeedFloat() * totalWeight; + + for (let i = 0; i < weights.length; i++) { + if (r < weights[i]) { + return i; + } + r -= weights[i]; + } + + return -1; // TODO: Change to something more appropriate +} + +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))); + return items[pickWeightedIndex(weights)]; +} + +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, + ); + + return items[pickWeightedIndex(weights)]; +} + +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 => m.getMove().type), + ); + 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 type = types[pickWeightedIndex(weights)]; + return attackTypeToHeldItem[type]; +} + +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 entry = pool[pickWeightedIndex(weights)].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 (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); + } + } + } + }); +} 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..3ffaad3bd60 --- /dev/null +++ b/src/items/held-item.ts @@ -0,0 +1,163 @@ +import { applyPostItemLostAbAttrs } 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 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 ITEM_EFFECT = (typeof ITEM_EFFECT)[keyof typeof 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`) + " (new)"; + } + + 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; + } + + getMaxStackCount(): number { + return this.maxStackCount; + } + + createSummaryIcon(pokemon: Pokemon): 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(pokemon); + 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(pokemon); + if (stackText) { + container.add(stackText); + } + + return container; + } + + getIconStackText(pokemon: Pokemon): Phaser.GameObjects.BitmapText | null { + if (this.getMaxStackCount() === 1) { + return null; + } + + const stackCount = pokemon.heldItemManager.getStack(this.type); + 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; + } +} + +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.updateModifiers(isPlayer); + } + if (unburden) { + applyPostItemLostAbAttrs("PostItemLostAbAttr", pokemon, false); + } + } +} diff --git a/src/items/held-items/accuracy-booster.ts b/src/items/held-items/accuracy-booster.ts new file mode 100644 index 00000000000..90325a18bc8 --- /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, 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: ITEM_EFFECT[] = [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..69015a0412e --- /dev/null +++ b/src/items/held-items/attack-type-booster.ts @@ -0,0 +1,77 @@ +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, 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 */ + 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: ITEM_EFFECT[] = [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; + } + + getName(): string { + return i18next.t(`modifierType:AttackTypeBoosterItem.${HeldItemNames[this.type]?.toLowerCase()}`); + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.AttackTypeBoosterModifierType.description", { + moveType: i18next.t(`pokemonInfo:Type.${PokemonType[this.moveType]}`), + }); + } + + getIcon(): 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..e024e74fd48 --- /dev/null +++ b/src/items/held-items/base-stat-booster.ts @@ -0,0 +1,88 @@ +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, ITEM_EFFECT } from "../held-item"; +import type { STAT_BOOST_PARAMS } from "./stat-booster"; + +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: ITEM_EFFECT[] = [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; + } + + getMaxStackCount(params: STAT_BOOST_PARAMS): number { + const pokemon = params.pokemon; + const stackCount = pokemon.heldItemManager.getStack(this.type); + return stackCount; + } +} 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..f191d8a06b9 --- /dev/null +++ b/src/items/held-items/base-stat-flat.ts @@ -0,0 +1,67 @@ +import type Pokemon from "#app/field/pokemon"; +import type { HeldItemId } from "#enums/held-item-id"; +import { HeldItem, ITEM_EFFECT } from "../held-item"; +import { getStatKey } 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: ITEM_EFFECT[] = [ITEM_EFFECT.BASE_STAT_FLAT]; + public isTransferable = false; + + constructor(type: HeldItemId, maxStackCount = 1) { + super(type, maxStackCount); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description", { + stats: this.stats.map(stat => i18next.t(getStatKey(stat))).join("/"), + statValue: this.statModifier, + }); + } + + /** + * 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 itemData = pokemon.heldItemManager.heldItems[this.type]?.data as BASE_STAT_FLAT_DATA; + if (!itemData) { + return false; + } + const statModifier = itemData.statModifier; + const stats = itemData.stats; + const baseStats = params.baseStats; + // 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; + } +} 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..4524441e64f --- /dev/null +++ b/src/items/held-items/base-stat-total.ts @@ -0,0 +1,78 @@ +import type Pokemon from "#app/field/pokemon"; +import i18next from "i18next"; +import { HeldItem, ITEM_EFFECT } from "../held-item"; +import type { BASE_STAT_TOTAL_DATA } from "../held-item-data"; + +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: ITEM_EFFECT[] = [ITEM_EFFECT.BASE_STAT_TOTAL]; + public isTransferable = false; + + get name(): string { + return i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE") + " (new)"; + } + + // TODO: where is this description shown? + get description(): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatTotalModifierType.description", { + increaseDecrease: i18next.t( + this.statModifier >= 0 + ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.increase" + : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.decrease", + ), + blessCurse: i18next.t( + this.statModifier >= 0 + ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.blessed" + : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.cursed", + ), + statValue: this.statModifier, + }); + } + + get iconName(): string { + return "berry_juice"; + } + + /** + * 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 pokemon = params.pokemon; + const itemData = pokemon.heldItemManager.heldItems[this.type]?.data as BASE_STAT_TOTAL_DATA; + if (!itemData) { + return false; + } + const statModifier = itemData.statModifier; + 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 + statModifier / 2) : Math.floor(v + 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..f8d5a8106ce --- /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, 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: ITEM_EFFECT[] = [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..d9d1f17ddfc --- /dev/null +++ b/src/items/held-items/berry.ts @@ -0,0 +1,92 @@ +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, ITEM_EFFECT } from "#app/items/held-item"; +import { PreserveBerryModifier } from "#app/modifier/modifier"; +import { BooleanHolder } from "#app/utils/common"; +import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; + +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: ITEM_EFFECT[] = [ITEM_EFFECT.BERRY]; + public berryType: BerryType; + + constructor(berryType: BerryType, maxStackCount = 1) { + const type = berryTypeToHeldItem[berryType]; + super(type, maxStackCount); + + this.berryType = berryType; + } + + get name(): string { + return getBerryName(this.berryType); + } + + get description(): string { + return getBerryEffectDescription(this.berryType); + } + + get iconName(): string { + return `${BerryType[this.berryType].toLowerCase()}_berry`; + } + + /** + * Checks if {@linkcode BerryModifier} should be applied + * @param pokemon The {@linkcode Pokemon} that holds the berry + * @returns `true` if {@linkcode BerryModifier} should be applied + */ + 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; + + const preserve = new BooleanHolder(false); + globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); + const consumed = !preserve.value; + + // munch the berry and trigger unburden-like effects + getBerryEffectFunc(this.berryType)(pokemon); + this.consume(pokemon, 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..6c1b2aba73e --- /dev/null +++ b/src/items/held-items/bypass-speed-chance.ts @@ -0,0 +1,62 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, 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: ITEM_EFFECT[] = [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..90502042844 --- /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, 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: ITEM_EFFECT[] = [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..46b9489f152 --- /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 { MoneyMultiplierModifier } from "#app/modifier/modifier"; +import { NumberHolder } from "#app/utils/common"; +import { HeldItem, ITEM_EFFECT } from "../held-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: ITEM_EFFECT[] = [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.applyModifiers(MoneyMultiplierModifier, true, 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..25e504819b8 --- /dev/null +++ b/src/items/held-items/evo-tracker.ts @@ -0,0 +1,82 @@ +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import type { NumberHolder } from "#app/utils/common"; +import { HeldItemId } from "#enums/held-item-id"; +import type { SpeciesId } from "#enums/species-id"; +import i18next from "i18next"; +import { HeldItem, ITEM_EFFECT } from "../held-item"; + +export interface EVO_TRACKER_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + multiplier: NumberHolder; +} + +//TODO: Possibly replace with this +export interface EVO_TRACKER_DATA { + evoCounter: number; +} + +export class EvoTrackerHeldItem extends HeldItem { + public effects: ITEM_EFFECT[] = [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; + } + + getIconStackText(pokemon: Pokemon): Phaser.GameObjects.BitmapText | null { + const stackCount = this.getStackCount(pokemon); + + const text = globalScene.add.bitmapText(10, 15, "item-count", stackCount.toString(), 11); + text.letterSpacing = -0.5; + if (stackCount >= this.required) { + text.setTint(0xf89890); + } + text.setOrigin(0, 0); + + return text; + } + + getStackCount(_pokemon: Pokemon): number { + return 0; + } +} + +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.evoCounter + + pokemon.heldItemManager.getStack(HeldItemId.GOLDEN_PUNCH) + + globalScene.findModifiers( + m => m.is("MoneyMultiplierModifier") || m.is("ExtraModifierModifier") || m.is("TempExtraModifierModifier"), + ).length; + 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..ce65f68571d --- /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, 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: ITEM_EFFECT[] = [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..babea90f4e7 --- /dev/null +++ b/src/items/held-items/field-effect.ts @@ -0,0 +1,33 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, 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: ITEM_EFFECT[] = [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..7f4a64bb507 --- /dev/null +++ b/src/items/held-items/flinch-chance.ts @@ -0,0 +1,57 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, 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: ITEM_EFFECT[] = [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..9820430d224 --- /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, 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: ITEM_EFFECT[] = [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..2117572e6cf --- /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, 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: ITEM_EFFECT[] = [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 && !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..e4ac0979c74 --- /dev/null +++ b/src/items/held-items/incrementing-stat.ts @@ -0,0 +1,70 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, 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; + statHolder: NumberHolder; +} + +/** + * Currently used by Macho Brace item + */ +export class IncrementingStatHeldItem extends HeldItem { + public effects: ITEM_EFFECT[] = [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..4290e2ede34 --- /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, 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: ITEM_EFFECT[] = [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..57bf222ff82 --- /dev/null +++ b/src/items/held-items/item-steal.ts @@ -0,0 +1,167 @@ +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, 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) */ + 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: ITEM_EFFECT[] = [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: ITEM_EFFECT[] = [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..244afd9a04e --- /dev/null +++ b/src/items/held-items/multi-hit.ts @@ -0,0 +1,89 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, 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: ITEM_EFFECT[] = [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..960433f929e --- /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, 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: ITEM_EFFECT[] = [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..5c28c235d44 --- /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, 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: ITEM_EFFECT[] = [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..0674cf83bc2 --- /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, 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: ITEM_EFFECT[] = [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) { + // 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 */ + private 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..848b766a72c --- /dev/null +++ b/src/items/held-items/survive-chance.ts @@ -0,0 +1,57 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, 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: ITEM_EFFECT[] = [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..ee78732a0d6 --- /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, 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: ITEM_EFFECT[] = [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..5352be4ca63 --- /dev/null +++ b/src/items/held-items/turn-end-status.ts @@ -0,0 +1,40 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, 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: ITEM_EFFECT[] = [ITEM_EFFECT.TURN_END_STATUS]; + /** The status effect to be applied by the held item */ + private 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/loading-scene.ts b/src/loading-scene.ts index f67d19e1027..f6bb11314d1 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -21,8 +21,10 @@ 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"; export class LoadingScene extends SceneBase { public static readonly KEY = "loading"; @@ -367,6 +369,7 @@ export class LoadingScene extends SceneBase { initModifierTypes(); initModifierPools(); + initHeldItemPools(); initAchievements(); initVouchers(); @@ -380,6 +383,7 @@ export class LoadingScene extends SceneBase { initSpecies(); initMoves(); initAbilities(); + initHeldItems(); initChallenges(); initMysteryEncounters(); } diff --git a/src/modifier/init-modifier-pools.ts b/src/modifier/init-modifier-pools.ts index 60697333600..d6d0b2a2fd6 100644 --- a/src/modifier/init-modifier-pools.ts +++ b/src/modifier/init-modifier-pools.ts @@ -1,20 +1,12 @@ import type Pokemon from "#app/field/pokemon"; -import { - dailyStarterModifierPool, - enemyBuffModifierPool, - modifierPool, - trainerModifierPool, - wildModifierPool, -} from "#app/modifier/modifier-pools"; +import { enemyBuffModifierPool, modifierPool } from "#app/modifier/modifier-pools"; import { globalScene } from "#app/global-scene"; -import { DoubleBattleChanceBoosterModifier, SpeciesCritBoosterModifier, TurnStatusEffectModifier } from "./modifier"; +import { DoubleBattleChanceBoosterModifier } 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 +18,14 @@ 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/items/all-held-items"; /** * 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 +57,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 +76,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)) @@ -128,7 +93,7 @@ function initCommonModifierPool() { 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 +102,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 +113,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].effect === p.status?.effect), ).length, 3, ); @@ -214,12 +177,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].effect === p.status?.effect), ).length, 3, ); @@ -239,7 +200,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 +219,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)) @@ -331,7 +292,7 @@ function initGreatModifierPool() { 1, ), ].map(m => { - m.setTier(ModifierTier.GREAT); + m.setTier(RewardTier.GREAT); return m; }); } @@ -340,7 +301,7 @@ 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.BIG_NUGGET, skipInLastClassicWaveOrDefault(12)), @@ -368,7 +329,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 +346,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 +359,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 +405,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 +451,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 +514,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 +558,7 @@ function initRogueModifierPool() { 3, ), ].map(m => { - m.setTier(ModifierTier.ROGUE); + m.setTier(RewardTier.ROGUE); return m; }); } @@ -611,7 +567,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,47 +600,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); + m.setTier(RewardTier.MASTER); return m; }); } @@ -693,7 +609,7 @@ function initTrainerModifierPool() { * Initialize the enemy buff modifier pool */ function initEnemyBuffModifierPool() { - enemyBuffModifierPool[ModifierTier.COMMON] = [ + enemyBuffModifierPool[RewardTier.COMMON] = [ new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 9), new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 9), new WeightedModifierType(modifierTypes.ENEMY_ATTACK_POISON_CHANCE, 3), @@ -703,20 +619,20 @@ function initEnemyBuffModifierPool() { new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 4), new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), ].map(m => { - m.setTier(ModifierTier.COMMON); + m.setTier(RewardTier.COMMON); return m; }); - enemyBuffModifierPool[ModifierTier.GREAT] = [ + enemyBuffModifierPool[RewardTier.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); + m.setTier(RewardTier.GREAT); return m; }); - enemyBuffModifierPool[ModifierTier.ULTRA] = [ + enemyBuffModifierPool[RewardTier.ULTRA] = [ new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 10), new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 10), new WeightedModifierType(modifierTypes.ENEMY_HEAL, 10), @@ -724,60 +640,15 @@ function initEnemyBuffModifierPool() { new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 10), new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 5), ].map(m => { - m.setTier(ModifierTier.ULTRA); + m.setTier(RewardTier.ULTRA); return m; }); - enemyBuffModifierPool[ModifierTier.ROGUE] = [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.ROGUE); + enemyBuffModifierPool[RewardTier.ROGUE] = [].map((m: WeightedModifierType) => { + m.setTier(RewardTier.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); + enemyBuffModifierPool[RewardTier.MASTER] = [].map((m: WeightedModifierType) => { + m.setTier(RewardTier.MASTER); return m; }); } @@ -795,10 +666,7 @@ export function initModifierPools() { initMasterModifierPool(); // Modifier pools for specific scenarios - initWildModifierPool(); - initTrainerModifierPool(); initEnemyBuffModifierPool(); - initDailyStarterModifierPool(); } /** diff --git a/src/modifier/modifier-bar.ts b/src/modifier/modifier-bar.ts new file mode 100644 index 00000000000..1581f26d63e --- /dev/null +++ b/src/modifier/modifier-bar.ts @@ -0,0 +1,133 @@ +import { formChangeItemName } from "#app/data/pokemon-forms"; +import type Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { allHeldItems } from "#app/items/all-held-items"; +import type { FormChangeItem } from "#enums/form-change-item"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { Modifier, PersistentModifier } from "./modifier"; + +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); + + //Then sort by item type + if (typeNameMatch === 0) { + return itemNameMatch; + //Finally sort by item name + } + return typeNameMatch; +}; + +//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; +}; + +export class ModifierBar extends Phaser.GameObjects.Container { + private player: boolean; + private modifierCache: (PersistentModifier | HeldItemId)[]; + private 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 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[], pokemonA?: Pokemon, pokemonB?: Pokemon) { + this.removeAll(true); + + const sortedVisibleModifiers = modifiers.filter(m => m.isIconVisible()).sort(modifierSortFunc); + + const heldItemsA = pokemonA ? pokemonA.getHeldItems().sort(heldItemSortFunc) : []; + const heldItemsB = pokemonB ? pokemonB.getHeldItems().sort(heldItemSortFunc) : []; + + this.totalVisibleLength = sortedVisibleModifiers.length + heldItemsA.length + heldItemsB.length; + + sortedVisibleModifiers.forEach((modifier: PersistentModifier, i: number) => { + const icon = modifier.getIcon(); + this.addIcon(icon, i, modifier.type.name, modifier.type.getDescription()); + }); + + heldItemsA.forEach((item: HeldItemId, i: number) => { + const icon = allHeldItems[item].createPokemonIcon(pokemonA); + this.addIcon(icon, i, allHeldItems[item].name, allHeldItems[item].description); + }); + + heldItemsB.forEach((item: HeldItemId, i: number) => { + const icon = allHeldItems[item].createPokemonIcon(pokemonB); + this.addIcon(icon, i, allHeldItems[item].name, allHeldItems[item].description); + }); + + for (const icon of this.getAll()) { + this.sendToBack(icon); + } + + this.modifierCache = (modifiers as (PersistentModifier | HeldItemId)[]).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.setModifierIconPosition(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.modifierCache && this.modifierCache.length > iconOverflowIndex) { + this.updateModifierOverflowVisibility(true); + } + }); + icon.on("pointerout", () => { + globalScene.ui.hideTooltip(); + if (this.modifierCache && this.modifierCache.length > iconOverflowIndex) { + this.updateModifierOverflowVisibility(false); + } + }); + } + + 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); + } +} diff --git a/src/modifier/modifier-pools.ts b/src/modifier/modifier-pools.ts index 3396dca1f93..db6ebc700e5 100644 --- a/src/modifier/modifier-pools.ts +++ b/src/modifier/modifier-pools.ts @@ -7,10 +7,6 @@ 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 a04a5e2be47..bb926ba789a 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1,30 +1,23 @@ 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 { formChangeItemName } from "#app/data/pokemon-forms"; import { getStatusEffectDescriptor } from "#app/data/status-effect"; 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, @@ -34,19 +27,15 @@ import { EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, - EvolutionStatBoosterModifier, - EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, - FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, - HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, @@ -54,50 +43,30 @@ import { 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"; @@ -124,15 +93,19 @@ import { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; import { timedEventManager } from "#app/global-event-manager"; +import { HeldItemId } from "#enums/held-item-id"; +import { allHeldItems } from "#app/items/all-held-items"; import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; +import { attackTypeToHeldItem } from "#app/items/held-items/attack-type-booster"; +import { permanentStatToHeldItem, statBoostItems } from "#app/items/held-items/base-stat-booster"; +import { SPECIES_STAT_BOOSTER_ITEMS, 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"; -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 +113,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 +151,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 +185,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 +322,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 +347,23 @@ 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); } } @@ -782,119 +727,35 @@ export class TempStatStageBoosterModifierType extends ModifierType implements Ge } } -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]; +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); + }); } } -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( @@ -930,104 +791,75 @@ export class AllPokemonLevelIncrementModifierType extends ModifierType { } } -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 -{ +export class BaseStatTotalHeldItemReward extends HeldItemReward { private readonly statModifier: number; - constructor(statModifier: number) { - super( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE", - "berry_juice", - (_type, args) => new PokemonBaseStatTotalModifier(this, (args[0] as Pokemon).id, this.statModifier), - ); + constructor(itemId: HeldItemId, statModifier: number) { + super(itemId); this.statModifier = statModifier; } - override getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonBaseStatTotalModifierType.description", { - increaseDecrease: i18next.t( - this.statModifier >= 0 - ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.increase" - : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.decrease", - ), - blessCurse: i18next.t( - this.statModifier >= 0 - ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.blessed" - : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.cursed", - ), - statValue: this.statModifier, - }); + apply(pokemon: Pokemon) { + super.apply(pokemon); + pokemon.heldItemManager[this.itemId].data.statModifier = this.statModifier; } +} - public getPregenArgs(): any[] { - return [this.statModifier]; +class BaseStatTotalHeldItemRewardGenerator extends ModifierTypeGenerator { + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new BaseStatTotalHeldItemReward(HeldItemId.SHUCKLE_JUICE, pregenArgs[0] as number); + } + return new BaseStatTotalHeldItemReward(HeldItemId.SHUCKLE_JUICE, randSeedInt(20, 1)); + }); } } /** * Old Gateau item */ -export class PokemonBaseStatFlatModifierType - extends PokemonHeldItemModifierType - implements GeneratedPersistentModifierType -{ +export class BaseStatFlatHeldItemReward extends HeldItemReward { private readonly statModifier: number; private readonly stats: Stat[]; - constructor(statModifier: number, stats: Stat[]) { - super( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU", - "old_gateau", - (_type, args) => new PokemonBaseStatFlatModifier(this, (args[0] as Pokemon).id, this.statModifier, this.stats), - ); + constructor(itemId: HeldItemId, statModifier: number, stats: Stat[]) { + super(itemId); this.statModifier = statModifier; this.stats = stats; } - override getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description", { - stats: this.stats.map(stat => i18next.t(getStatKey(stat))).join("/"), - statValue: this.statModifier, - }); + apply(pokemon: Pokemon) { + super.apply(pokemon); + pokemon.heldItemManager[this.itemId].data.statModifier = this.statModifier; } +} - public getPregenArgs(): any[] { - return [this.statModifier, this.stats]; +class BaseStatFlatHeldItemRewardGenerator extends ModifierTypeGenerator { + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new BaseStatFlatHeldItemReward(HeldItemId.OLD_GATEAU, pregenArgs[0] as number, pregenArgs[1] as Stat[]); + } + return new BaseStatFlatHeldItemReward(HeldItemId.OLD_GATEAU, randSeedInt(20, 1), [Stat.HP, Stat.ATK, Stat.DEF]); + }); } } @@ -1101,72 +933,6 @@ export class ExpBoosterModifierType extends ModifierType { } } -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; @@ -1258,14 +1024,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 ( @@ -1291,16 +1057,12 @@ 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]; - } } export class FusePokemonModifierType extends PokemonModifierType { @@ -1323,80 +1085,27 @@ 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()); }); } } @@ -1428,66 +1137,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; @@ -1495,18 +1159,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]].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))) { @@ -1520,6 +1177,7 @@ class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { } } + // TODO: Replace this with a helper function let totalWeight = 0; for (const weight of weights) { totalWeight += weight; @@ -1533,7 +1191,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; } @@ -1546,7 +1204,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); @@ -1619,11 +1277,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 = [ @@ -1647,16 +1305,7 @@ export class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { 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... @@ -1699,49 +1348,11 @@ export class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { return null; } - return new FormChangeItemModifierType(formChangeItemPool[randSeedInt(formChangeItemPool.length)]); + return new FormChangeItemReward(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; @@ -1798,7 +1409,7 @@ export class WeightedModifierType { this.maxWeight = maxWeight || (!(weight instanceof Function) ? weight : 0); } - setTier(tier: ModifierTier) { + setTier(tier: RewardTier) { this.modifierType.setTier(tier); } } @@ -1814,7 +1425,7 @@ export type GeneratorModifierOverride = { } & ( | { name: keyof Pick; - type?: SpeciesStatBoosterItem; + type?: SpeciesStatBoosterItemId; } | { name: keyof Pick; @@ -1867,16 +1478,11 @@ 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), + + EVOLUTION_TRACKER_GIMMIGHOUL: () => new HeldItemReward(HeldItemId.GIMMIGHOUL_EVO_TRACKER), MEGA_BRACELET: () => new ModifierType( @@ -1915,18 +1521,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), @@ -1945,8 +1542,8 @@ const modifierTypeInitObj = Object.freeze({ SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 15), MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 30), - 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(), @@ -1960,9 +1557,9 @@ const modifierTypeInitObj = Object.freeze({ } })("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new TempCritBoosterModifier(type, 5)), - 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[]) => { @@ -1972,12 +1569,7 @@ 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[]) => { @@ -2006,29 +1598,11 @@ 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"), @@ -2053,42 +1627,17 @@ const modifierTypeInitObj = Object.freeze({ GOLDEN_EXP_CHARM: () => new ExpBoosterModifierType("modifierType:ModifierType.GOLDEN_EXP_CHARM", "golden_exp_charm", 100), - 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( @@ -2118,12 +1667,8 @@ const modifierTypeInitObj = Object.freeze({ "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), - ), + GOLDEN_PUNCH: () => new HeldItemReward(HeldItemId.GOLDEN_PUNCH), + COIN_CASE: () => new ModifierType( "modifierType:ModifierType.COIN_CASE", @@ -2138,11 +1683,10 @@ const modifierTypeInitObj = Object.freeze({ (type, _args) => new LockModifierTiersModifier(type), ), - 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( @@ -2164,59 +1708,21 @@ const modifierTypeInitObj = Object.freeze({ (type, _args) => new PreserveBerryModifier(type), ), - 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), + + FLAME_ORB: () => new HeldItemReward(HeldItemId.FLAME_ORB), + + BATON: () => new HeldItemReward(HeldItemId.BATON), SHINY_CHARM: () => new ModifierType( @@ -2242,8 +1748,7 @@ const modifierTypeInitObj = Object.freeze({ 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), @@ -2324,20 +1829,10 @@ const modifierTypeInitObj = Object.freeze({ (type, _args) => new EnemyFusionChanceModifier(type, 1), ), - MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () => - new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs) { - return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number); - } - return new PokemonBaseStatTotalModifierType(randSeedInt(20, 1)); - }), - MYSTERY_ENCOUNTER_OLD_GATEAU: () => - new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs) { - return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]); - } - return new PokemonBaseStatFlatModifierType(randSeedInt(20, 1), [Stat.HP, Stat.ATK, Stat.DEF]); - }), + MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () => new BaseStatTotalHeldItemRewardGenerator(), + + MYSTERY_ENCOUNTER_OLD_GATEAU: () => new BaseStatFlatHeldItemRewardGenerator(), + MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs) { @@ -2353,12 +1848,8 @@ const modifierTypeInitObj = Object.freeze({ (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_MACHO_BRACE: () => new HeldItemReward(HeldItemId.MACHO_BRACE), + MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => new ModifierType( "modifierType:ModifierType.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET", @@ -2379,19 +1870,10 @@ 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. */ @@ -2404,14 +1886,12 @@ 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; @@ -2425,8 +1905,8 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod : weightedModifierType.modifierType; const weight = !existingModifiers.length || - itemModifierType instanceof PokemonHeldItemModifierType || - itemModifierType instanceof FormChangeItemModifierType || + itemModifierType instanceof HeldItemReward || + itemModifierType instanceof FormChangeItemReward || existingModifiers.find(m => m.stackCount < m.getMaxStackCount(true)) ? weightedModifierType.weight instanceof Function ? // biome-ignore lint/complexity/noBannedTypes: TODO: refactor to not use Function type @@ -2436,14 +1916,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; @@ -2457,44 +1929,24 @@ 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 { - guaranteedModifierTiers?: ModifierTier[]; + guaranteedModifierTiers?: RewardTier[]; guaranteedModifierTypeOptions?: ModifierTypeOption[]; guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; fillRemaining?: boolean; @@ -2515,9 +1967,9 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc { * @param customModifierSettings (Optional) If specified, can customize the item shop rewards further. * - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` If specified, will override the first X items to be specific modifier options (these should be pre-genned). * - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). - * - `guaranteedModifierTiers?: ModifierTier[]` If specified, will override the next X items to be the specified tier. These can upgrade with luck. + * - `guaranteedModifierTiers?: RewardTier[]` If specified, will override the next X items to be the specified tier. These can upgrade with luck. * - `fillRemaining?: boolean` Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value. - * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`, + * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [RewardTier.GREAT], fillRemaining: true }`, * - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally. * - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value). * - `rerollMultiplier?: number` If specified, can adjust the amount of money required for a shop reroll. If set to a negative value, the shop will not allow rerolls at all. @@ -2526,7 +1978,7 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc { export function getPlayerModifierTypeOptions( count: number, party: PlayerPokemon[], - modifierTiers?: ModifierTier[], + modifierTiers?: RewardTier[], customModifierSettings?: CustomModifierSettings, ): ModifierTypeOption[] { const options: ModifierTypeOption[] = []; @@ -2607,7 +2059,7 @@ function getModifierTypeOptionWithRetry( existingOptions: ModifierTypeOption[], retryCount: number, party: PlayerPokemon[], - tier?: ModifierTier, + tier?: RewardTier, allowLuckUpgrades?: boolean, ): ModifierTypeOption { allowLuckUpgrades = allowLuckUpgrades ?? true; @@ -2618,6 +2070,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, @@ -2690,15 +2147,15 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseC } export function getEnemyBuffModifierForWave( - tier: ModifierTier, + tier: RewardTier, enemyModifiers: PersistentModifier[], ): EnemyPersistentModifier { let tierStackCount: number; switch (tier) { - case ModifierTier.ULTRA: + case RewardTier.ULTRA: tierStackCount = 5; break; - case ModifierTier.GREAT: + case RewardTier.GREAT: tierStackCount = 3; break; default: @@ -2724,55 +2181,6 @@ export function getEnemyBuffModifierForWave( 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 @@ -2785,85 +2193,16 @@ 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; - } - 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); - } + const thresholds = getPoolThresholds(poolType); - 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--; - } + const tier = determineTier(party, player, baseTier, upgradeCount, retryCount, allowLuckUpgrades); const tierThresholds = Object.keys(thresholds[tier]); const totalWeight = Number.parseInt(tierThresholds[tierThresholds.length - 1]); @@ -2889,7 +2228,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); } @@ -2900,9 +2239,88 @@ 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; + case ModifierPoolType.ENEMY_BUFF: + thresholds = enemyBuffModifierPoolThresholds; + break; + } + return thresholds; +} + +function determineTier( + party: Pokemon[], + player: boolean, + tier?: RewardTier, + upgradeCount?: number, + retryCount = 0, + allowLuckUpgrades = true, +): RewardTier { + const pool = getModifierPoolForType(ModifierPoolType.PLAYER); + 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 = RewardTier.COMMON; + } else if (tierValue > 60) { + tier = RewardTier.GREAT; + } else if (tierValue > 12) { + tier = RewardTier.ULTRA; + } else if (tierValue) { + tier = RewardTier.ROGUE; + } else { + tier = RewardTier.MASTER; + } + + tier += upgradeCount; + while (tier && (!pool.hasOwnProperty(tier) || !pool[tier].length)) { + tier--; + if (upgradeCount) { + upgradeCount--; + } + } + } else if (upgradeCount === undefined && player) { + upgradeCount = 0; + if (tier < RewardTier.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--; + } + return tier; +} + +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; } @@ -2954,19 +2372,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); } @@ -2981,7 +2399,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 54b7323569a..204023b159c 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1,156 +1,47 @@ 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 type { PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; 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 { + type 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 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 { type TempBattleStat, 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, applyPostItemLostAbAttrs } 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 { modifierTypes } from "#app/data/data-lists"; 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; @@ -674,1133 +565,6 @@ export class TerastallizeAccessModifier extends PersistentModifier { } } -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 this.pokemonId ? (globalScene.getPokemonById(this.pokemonId) ?? undefined) : 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; - - private statModifier: number; - - constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: number, 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 { - private statModifier: number; - private stats: Stat[]; - public isTransferable = false; - - constructor(type: ModifierType, pokemonId: number, statModifier: number, stats: Stat[], stackCount?: number) { - super(type, pokemonId, stackCount); - - this.statModifier = statModifier; - this.stats = stats; - } - - override matchType(modifier: Modifier): boolean { - return ( - modifier instanceof PokemonBaseStatFlatModifier && - modifier.statModifier === this.statModifier && - this.stats.every(s => modifier.stats.some(stat => s === stat)) - ); - } - - override clone(): PersistentModifier { - return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount); - } - - override getArgs(): any[] { - return [...super.getArgs(), this.statModifier, this.stats]; - } - - /** - * 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 - baseStats.forEach((v, i) => { - if (this.stats.includes(i)) { - const newVal = Math.floor(v + this.statModifier); - baseStats[i] = Math.min(Math.max(newVal, 1), 999999); - } - }); - - return true; - } - - 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; @@ -1835,67 +599,6 @@ export class LevelIncrementBoosterModifier extends PersistentModifier { } } -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); - applyPostItemLostAbAttrs("PostItemLostAbAttr", pokemon, false); - - // 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; @@ -1932,130 +635,6 @@ export class PreserveBerryModifier extends PersistentModifier { } } -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", p, null, false); - } - 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; @@ -2543,59 +1122,6 @@ export class ExpBoosterModifier extends PersistentModifier { } } -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; @@ -2640,258 +1166,6 @@ export class ExpBalanceModifier extends PersistentModifier { } } -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; @@ -2952,34 +1226,6 @@ export class MoneyMultiplierModifier extends PersistentModifier { } } -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; @@ -3175,188 +1421,6 @@ export class BoostBugSpawnModifier extends PersistentModifier { } } -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); - } - } - - 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); @@ -3782,7 +1846,7 @@ export function overrideModifiers(isPlayer = true): void { const modifierFunc = modifierTypes[item.name]; let modifierType: ModifierType | null = modifierFunc(); - if (modifierType.is("ModifierTypeGenerator")) { + if (modifierType?.is("ModifierTypeGenerator")) { const pregenArgs = "type" in item && item.type !== null ? [item.type] : undefined; modifierType = modifierType.generateType([], pregenArgs); } @@ -3794,7 +1858,7 @@ export function overrideModifiers(isPlayer = true): void { if (isPlayer) { globalScene.addModifier(modifier, true, false, false, true); } else { - globalScene.addEnemyModifier(modifier, true, true); + globalScene.addEnemyModifier(modifier, true); } } } @@ -3808,7 +1872,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 +1880,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); } /** @@ -3863,30 +1906,8 @@ const ModifierClassMap = Object.freeze({ 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, @@ -3904,16 +1925,8 @@ const ModifierClassMap = Object.freeze({ MultipleParticipantExpBonusModifier, HealingBoosterModifier, ExpBoosterModifier, - PokemonExpBoosterModifier, ExpShareModifier, ExpBalanceModifier, - PokemonFriendshipBoosterModifier, - PokemonNatureWeightModifier, - PokemonMoveAccuracyBoosterModifier, - PokemonMultiHitModifier, - PokemonFormChangeItemModifier, - MoneyRewardModifier, - DamageMoneyRewardModifier, MoneyInterestModifier, HiddenAbilityRateBoosterModifier, ShinyRateBoosterModifier, @@ -3921,10 +1934,6 @@ const ModifierClassMap = Object.freeze({ LockModifierTiersModifier, HealShopCostModifier, BoostBugSpawnModifier, - SwitchEffectTransferModifier, - HeldItemTransferModifier, - TurnHeldItemTransferModifier, - ContactHeldItemTransferChanceModifier, IvScannerModifier, ExtraModifierModifier, TempExtraModifierModifier, diff --git a/src/overrides.ts b/src/overrides.ts index b390b9fa70f..546935d1c45 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -22,6 +22,7 @@ import { TimeOfDay } from "#enums/time-of-day"; import { TrainerType } from "#enums/trainer-type"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; +import { HeldItemConfiguration } from "./items/held-item-data-types"; /** * This comment block exists to prevent IDEs from automatically removing unused imports @@ -255,9 +256,9 @@ class DefaultOverrides { 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_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..193172f7aba 100644 --- a/src/phases/add-enemy-buff-modifier-phase.ts +++ b/src/phases/add-enemy-buff-modifier-phase.ts @@ -1,4 +1,4 @@ -import { ModifierTier } from "#enums/modifier-tier"; +import { RewardTier } from "#enums/reward-tier"; import { regenerateModifierPoolThresholds, getEnemyBuffModifierForWave } from "#app/modifier/modifier-type"; import { ModifierPoolType } from "#enums/modifier-pool-type"; import { EnemyPersistentModifier } from "#app/modifier/modifier"; @@ -11,11 +11,7 @@ export class AddEnemyBuffModifierPhase extends Phase { super.start(); const waveIndex = globalScene.currentBattle.waveIndex; - const tier = !(waveIndex % 1000) - ? ModifierTier.ULTRA - : !(waveIndex % 250) - ? ModifierTier.GREAT - : ModifierTier.COMMON; + const tier = !(waveIndex % 1000) ? RewardTier.ULTRA : !(waveIndex % 250) ? RewardTier.GREAT : RewardTier.COMMON; regenerateModifierPoolThresholds(globalScene.getEnemyParty(), ModifierPoolType.ENEMY_BUFF); @@ -27,10 +23,9 @@ export class AddEnemyBuffModifierPhase extends Phase { globalScene.findModifiers(m => m instanceof EnemyPersistentModifier, false), ), true, - true, ); } - globalScene.updateModifiers(false, true); + globalScene.updateModifiers(false); this.end(); } } diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index f4e6725935a..95a0317f4a4 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.updateModifiers(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 5e24f3474a6..58e696e1b34 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -43,8 +43,6 @@ export class AttemptRunPhase extends PokemonPhase { enemyField.forEach(enemyPokemon => enemyPokemon.destroy()), }); - globalScene.clearEnemyHeldItemModifiers(); - // biome-ignore lint/complexity/noForEach: TODO enemyField.forEach(enemyPokemon => { enemyPokemon.hideInfo().then(() => enemyPokemon.destroy()); diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index e1bf4c2296c..9a0f3b4e5f9 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import { applyPostBattleAbAttrs } from "#app/data/abilities/apply-ab-attrs"; -import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; +import { LapsingPersistentModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; export class BattleEndPhase extends BattlePhase { @@ -72,7 +72,6 @@ export class BattleEndPhase extends BattlePhase { globalScene.currentBattle.pickUpScatteredMoney(); } - globalScene.clearEnemyHeldItemModifiers(); for (const p of globalScene.getEnemyParty()) { try { p.destroy(); @@ -82,13 +81,10 @@ export class BattleEndPhase extends BattlePhase { } const lapsingModifiers = globalScene.findModifiers( - m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier, - ) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[]; + m => m instanceof LapsingPersistentModifier, + ) as LapsingPersistentModifier[]; 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); } diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index c126f3306b9..a4d63cf769e 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 { 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,10 @@ 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 => { + //TODO: This is bugged, must fix the .shouldApply() function + isItemInCategory(m, HeldItemCategoryId.BERRY) && allHeldItems[m].shouldApply(pokemon); + }); if (!hasUsableBerry) { return; @@ -59,14 +60,7 @@ 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)); - } + applyHeldItems(ITEM_EFFECT.BERRY, { pokemon: pokemon }); globalScene.updateModifiers(pokemon.isPlayer()); // AbilityId.CHEEK_POUCH only works once per round of nom noms diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index f2c23384627..9ddddaef76d 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -14,9 +14,7 @@ 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 { BoostBugSpawnModifier, IvScannerModifier } from "#app/modifier/modifier"; import Overrides from "#app/overrides"; import { BattlePhase } from "#app/phases/battle-phase"; import { achvs } from "#app/system/achv"; @@ -271,10 +269,6 @@ 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); @@ -543,22 +537,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)) { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 675a198d096..1212f9e26c5 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -18,13 +18,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 { ITEM_EFFECT } from "#app/items/held-item"; export class FaintPhase extends PokemonPhase { public readonly phaseName = "FaintPhase"; @@ -58,17 +59,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(ITEM_EFFECT.INSTANT_REVIVE, { pokemon: faintPokemon }); } /** diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index d7da1ab996c..70996f55a19 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -33,15 +33,7 @@ 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 { EnemyAttackStatusEffectChanceModifier, EnemyEndureChanceModifier } 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"; @@ -54,6 +46,8 @@ 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 { ITEM_EFFECT } from "#app/items/held-item"; import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode"; export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; @@ -324,7 +318,7 @@ export class MoveEffectPhase extends PokemonPhase { // If Parental Bond is applicable, add another hit applyPreAttackAbAttrs("AddSecondStrikeAbAttr", user, null, move, false, hitCount, null); // 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(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; @@ -424,7 +418,7 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); } - globalScene.applyModifiers(HitHealModifier, this.player, user); + applyHeldItems(ITEM_EFFECT.HIT_HEAL, { pokemon: user }); this.getTargets().forEach(target => { target.turnData.moveEffectiveness = null; }); @@ -460,7 +454,7 @@ export class MoveEffectPhase extends PokemonPhase { !this.move.hitsSubstitute(user, target) ) { const flinched = new BooleanHolder(false); - globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + applyHeldItems(ITEM_EFFECT.FLINCH_CHANCE, { pokemon: user, flinched: flinched }); if (flinched.value) { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.id, user.id); } @@ -905,7 +899,7 @@ export class MoveEffectPhase extends PokemonPhase { }); if (user.isPlayer() && target.isEnemy()) { - globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage)); + applyHeldItems(ITEM_EFFECT.DAMAGE_MONEY_REWARD, { pokemon: user, damage: damage }); } return result; @@ -1011,7 +1005,7 @@ export class MoveEffectPhase extends PokemonPhase { // 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(ITEM_EFFECT.CONTACT_ITEM_STEAL_CHANCE, { pokemon: user, target: target }); } } } diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 53e1f5bc282..e6aa1d4d1fb 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,11 @@ import { PokemonPpRestoreModifierType, PokemonPpUpModifierType, getPlayerModifierTypeOptions, + HeldItemReward, } 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 { ExtraModifierModifier, HealShopCostModifier, 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"; @@ -36,7 +32,7 @@ export type ModifierSelectCallback = (rowCursor: number, cursor: number) => bool 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 +40,7 @@ export class SelectModifierPhase extends BattlePhase { constructor( rerollCount = 0, - modifierTiers?: ModifierTier[], + modifierTiers?: RewardTier[], customModifierSettings?: CustomModifierSettings, isCopy = false, ) { @@ -172,7 +168,9 @@ export class SelectModifierPhase extends BattlePhase { modifierSelectCallback: ModifierSelectCallback, ): boolean { if (modifierType instanceof PokemonModifierType) { - if (modifierType instanceof FusePokemonModifierType) { + if (modifierType instanceof HeldItemReward) { + this.openGiveHeldItemMenu(modifierType, modifierSelectCallback); + } else if (modifierType instanceof FusePokemonModifierType) { this.openFusionMenu(modifierType, cost, modifierSelectCallback); } else { this.openModifierMenu(modifierType, cost, modifierSelectCallback); @@ -194,7 +192,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 +220,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 { @@ -372,6 +368,29 @@ 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); @@ -456,6 +475,7 @@ export class SelectModifierPhase extends BattlePhase { } getModifierTypeOptions(modifierCount: number): ModifierTypeOption[] { + console.log("HERE WE ARE", modifierCount, this.customModifierSettings); return getPlayerModifierTypeOptions( modifierCount, globalScene.getPlayerParty(), diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index e73f72f7a63..5effb11adc2 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -10,7 +10,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"; @@ -18,6 +17,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 { ITEM_EFFECT } from "#app/items/held-item"; export type StatStageChangeCallback = ( target: Pokemon | null, @@ -240,16 +241,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(ITEM_EFFECT.RESET_NEGATIVE_STAT_STAGE, { pokemon: pokemon, isPlayer: this.player }); } pokemon.updateInfo(); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index af03cc42b54..c9ad16ae963 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..61b2cf05ecf 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -6,9 +6,7 @@ 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 +18,7 @@ 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"; export class TitlePhase extends Phase { public readonly phaseName = "TitlePhase"; @@ -238,8 +237,6 @@ export class TitlePhase extends Phase { loadPokemonAssets.push(starterPokemon.loadAssets()); } - regenerateModifierPoolThresholds(party, ModifierPoolType.DAILY_STARTER); - const modifiers: Modifier[] = Array(3) .fill(null) .map(() => modifierTypes.EXP_SHARE().withIdFromFunc(modifierTypes.EXP_SHARE).newModifier()) @@ -249,13 +246,14 @@ export class TitlePhase extends Phase { .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.updateModifiers(true); Promise.all(loadPokemonAssets).then(() => { globalScene.time.delayedCall(500, () => globalScene.playBgm()); diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index ab46292c1d2..560133bb7c1 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 { EnemyTurnHealModifier, EnemyStatusEffectHealChanceModifier } 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 { ITEM_EFFECT } from "#app/items/held-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(ITEM_EFFECT.TURN_END_HEAL, { pokemon: pokemon }); if (globalScene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { globalScene.phaseManager.unshiftNew( @@ -52,8 +48,9 @@ export class TurnEndPhase extends FieldPhase { applyPostTurnAbAttrs("PostTurnAbAttr", pokemon); } - globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); - globalScene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); + applyHeldItems(ITEM_EFFECT.TURN_END_STATUS, { pokemon: pokemon }); + + applyHeldItems(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 6219907fb68..b9e2e86809d 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -4,7 +4,6 @@ import { AbilityId } from "#enums/ability-id"; 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"; @@ -12,6 +11,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 { ITEM_EFFECT } from "#app/items/held-item"; +import { applyHeldItems } from "#app/items/all-held-items"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; @@ -69,7 +70,7 @@ export class TurnStartPhase extends FieldPhase { applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed); applyAbAttrs("PreventBypassSpeedChanceAbAttr", p, null, false, bypassSpeed, canCheckHeldItems); if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); + applyHeldItems(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/ui/battle-flyout.ts b/src/ui/battle-flyout.ts index 9a2180eccee..7356666e6c5 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/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 7f5bf997f88..24ddbdaef05 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -5,7 +5,7 @@ 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 { LockModifierTiersModifier, HealShopCostModifier } from "../modifier/modifier"; import { handleTutorial, Tutorial } from "../tutorial"; import { Button } from "#enums/buttons"; import MoveInfoOverlay from "./move-info-overlay"; @@ -183,7 +183,10 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.player = args[0]; const partyHasHeldItem = - this.player && !!globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable).length; + globalScene + .getPlayerParty() + .map(p => p.heldItemManager.getTransferableHeldItems().length) + .reduce((tot, i) => tot + i, 0) > 0; const canLockRarities = !!globalScene.findModifier(m => m instanceof LockModifierTiersModifier); this.transferButtonContainer.setVisible(false); @@ -216,6 +219,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { : []; const optionsYOffset = shopTypeOptions.length > SHOP_OPTIONS_ROW_LIMIT ? -SINGLE_SHOP_ROW_YOFFSET : -DOUBLE_SHOP_ROW_YOFFSET; + console.log("ui_shop_options", shopTypeOptions); for (let m = 0; m < typeOptions.length; m++) { const sliceWidth = globalScene.game.canvas.width / 6 / (typeOptions.length + 2); @@ -229,6 +233,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.modifierContainer.add(option); this.options.push(option); } + console.log("ui_options", this.options); // Set "Continue" button height based on number of rows in healing items shop const continueButton = this.continueButtonContainer.getAt(0); @@ -258,10 +263,13 @@ 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); + //TODO: temporary stopgap so the game does not crash, will have to fix this later + // console.log(typeOptions.map(to => to.upgradeCount)) + // const maxUpgradeCount = typeOptions.map(to => to.upgradeCount).reduce((max, current) => Math.max(current, max), 0); + const maxUpgradeCount = 0; - /* Force updateModifiers without pokemonSpecificModifiers */ - globalScene.getModifierBar().updateModifiers(globalScene.modifiers, true); + /* Force updateModifiers without pokemon held items */ + globalScene.getModifierBar().updateModifiers(globalScene.modifiers); /* 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); @@ -288,14 +296,20 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { }, }); + console.log("maxUpgradeCount", maxUpgradeCount); + globalScene.time.delayedCall(1000 + maxUpgradeCount * 2000, () => { + console.log("delayed1", partyHasHeldItem); for (const shopOption of this.shopOptionsRows.flat()) { + console.log("uishop", shopOption); shopOption.show(0, 0); } }); globalScene.time.delayedCall(4000 + maxUpgradeCount * 2000, () => { + console.log("delayed2", partyHasHeldItem); if (partyHasHeldItem) { + console.log("uihelditem", partyHasHeldItem); this.transferButtonContainer.setAlpha(0); this.transferButtonContainer.setVisible(true); globalScene.tweens.add({ @@ -350,6 +364,8 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { }); }); + console.log("ui_end"); + return true; } @@ -763,7 +779,8 @@ class ModifierOption extends Phaser.GameObjects.Container { this.add(this.itemContainer); const getItem = () => { - const item = globalScene.add.sprite(0, 0, "items", this.modifierTypeOption.type?.iconImage); + console.log("SHOWING ICON", this.modifierTypeOption.type?.name, this.modifierTypeOption.type?.getIcon()); + const item = globalScene.add.sprite(0, 0, "items", this.modifierTypeOption.type?.getIcon()); return item; }; @@ -798,6 +815,7 @@ class ModifierOption extends Phaser.GameObjects.Container { } show(remainingDuration: number, upgradeCountOffset: number) { + console.log("mo1"); if (!this.modifierTypeOption.cost) { globalScene.tweens.add({ targets: this.pb, @@ -805,6 +823,7 @@ class ModifierOption extends Phaser.GameObjects.Container { duration: 1250, ease: "Bounce.Out", }); + console.log("mo2"); let lastValue = 1; let bounceCount = 0; @@ -831,6 +850,7 @@ class ModifierOption extends Phaser.GameObjects.Container { lastValue = value; }, }); + console.log("mo3"); for (let u = 0; u < this.modifierTypeOption.upgradeCount; u++) { const upgradeIndex = u; @@ -868,12 +888,14 @@ class ModifierOption extends Phaser.GameObjects.Container { }, ); } + console.log("mo4"); } globalScene.time.delayedCall(remainingDuration + 2000, () => { if (!globalScene) { return; } + console.log("mo5"); if (!this.modifierTypeOption.cost) { this.pb.setTexture("pb", `${this.getPbAtlasKey(0)}_open`); @@ -888,6 +910,7 @@ class ModifierOption extends Phaser.GameObjects.Container { onComplete: () => this.pb.destroy(), }); } + console.log("mo6"); globalScene.tweens.add({ targets: this.itemContainer, @@ -922,6 +945,7 @@ class ModifierOption extends Phaser.GameObjects.Container { }); } }); + console.log("mo_end"); } getPbAtlasKey(tierOffset = 0) { diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index cf6333f4580..95d1072346a 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -7,7 +7,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"; @@ -28,6 +27,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/items/all-held-items"; const defaultMessage = i18next.t("partyUiHandler:choosePokemon"); @@ -138,10 +140,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 { @@ -220,11 +219,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; @@ -503,8 +499,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, @@ -512,8 +510,7 @@ export default class PartyUiHandler extends MessageUiHandler { this.transferQuantitiesMax[invertedIndex], this.cursor, ); - }, - ); + }); } else { (this.selectCallback as PartyModifierTransferSelectCallback)( this.transferCursor, @@ -548,18 +545,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 { @@ -681,12 +675,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) { @@ -700,7 +688,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 ], ); @@ -751,9 +739,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); } @@ -922,14 +910,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(); @@ -1160,9 +1144,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) ); } @@ -1177,14 +1159,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++) { @@ -1204,11 +1178,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); } } @@ -1335,8 +1309,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); } } @@ -1397,10 +1371,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 { @@ -1424,9 +1398,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; @@ -1438,19 +1412,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]`; } @@ -1499,7 +1473,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(); @@ -1560,29 +1533,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 78ab4a40407..bbb13033db7 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -27,6 +27,7 @@ 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 { modifierSortFunc } from "#app/modifier/modifier-bar"; /** * RunInfoUiMode indicates possible overlays of RunInfoUiHandler. @@ -894,7 +895,7 @@ export default class RunInfoUiHandler extends UiHandler { } } if (heldItemsList.length > 0) { - (heldItemsList as Modifier.PokemonHeldItemModifier[]).sort(Modifier.modifierSortFunc); + (heldItemsList as Modifier.PokemonHeldItemModifier[]).sort(modifierSortFunc); let row = 0; for (const [index, item] of heldItemsList.entries()) { if (index > 36) { diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index d30322de293..17ccfdc5118 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 { heldItemSortFunc } from "#app/modifier/modifier-bar"; 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/items/all-held-items"; enum Page { PROFILE, @@ -1032,24 +1032,36 @@ 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]; + 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(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 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(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 8812d8ee4a8..00351cd3585 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/modifier-utils.ts b/src/utils/modifier-utils.ts index 3be4af3730c..dfd04fa852e 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 { dailyStarterModifierPool, enemyBuffModifierPool, 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,10 +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: diff --git a/test/items/lock_capsule.test.ts b/test/items/lock_capsule.test.ts index beacc3a3907..cbf8d2ca7e9 100644 --- a/test/items/lock_capsule.test.ts +++ b/test/items/lock_capsule.test.ts @@ -1,6 +1,6 @@ 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"; @@ -36,7 +36,7 @@ describe("Items - Lock Capsule", () => { 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/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 85193d1ec72..818cd0c923c 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); 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..15e46d80634 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"; @@ -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); }); 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/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 b9f499d4e0c..a8874611c75 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -546,7 +546,6 @@ export default class GameManager { * Removes all held items from enemy pokemon. */ removeEnemyHeldItems(): void { - this.scene.clearEnemyHeldItemModifiers(); this.scene.clearEnemyModifiers(); console.log("Enemy held items removed"); }