diff --git a/index.css b/index.css index 62ad6266d30..e7439639d3a 100644 --- a/index.css +++ b/index.css @@ -183,7 +183,7 @@ input:-internal-autofill-selected { /* Show #apadStats only in battle and shop */ #touchControls:not([data-ui-mode="COMMAND"]):not([data-ui-mode="FIGHT"]):not( [data-ui-mode="BALL"] - ):not([data-ui-mode="TARGET_SELECT"]):not([data-ui-mode="MODIFIER_SELECT"]) + ):not([data-ui-mode="TARGET_SELECT"]):not([data-ui-mode="REWARD_SELECT"]) #apadStats { display: none; } diff --git a/src/@types/held-modifier-config.ts b/src/@types/held-modifier-config.ts deleted file mode 100644 index 46ef67bf515..00000000000 --- a/src/@types/held-modifier-config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { PokemonHeldItemModifier } from "#modifiers/modifier"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; - -export interface HeldModifierConfig { - modifier: PokemonHeldItemModifierType | PokemonHeldItemModifier; - stackCount?: number; - isTransferable?: boolean; -} diff --git a/src/@types/locales.ts b/src/@types/locales.ts index 3b5a1477e19..af3d5fcc596 100644 --- a/src/@types/locales.ts +++ b/src/@types/locales.ts @@ -24,15 +24,15 @@ export interface AbilityTranslationEntries { [key: string]: AbilityTranslationEntry; } -export interface ModifierTypeTranslationEntry { +export interface RewardTranslationEntry { name?: string; description?: string; extra?: SimpleTranslationEntries; } -export interface ModifierTypeTranslationEntries { - ModifierType: { [key: string]: ModifierTypeTranslationEntry }; - SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry }; +export interface RewardTranslationEntries { + Reward: { [key: string]: RewardTranslationEntry }; + SpeciesBoosterItem: { [key: string]: RewardTranslationEntry }; AttackTypeBoosterItem: SimpleTranslationEntries; TempStatStageBoosterItem: SimpleTranslationEntries; BaseStatBoosterItem: SimpleTranslationEntries; diff --git a/src/@types/modifier-types.ts b/src/@types/modifier-types.ts deleted file mode 100644 index 13a84a984e2..00000000000 --- a/src/@types/modifier-types.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Intentionally re-exports `ModifierConstructorMap` from `modifier.ts` - -import type { Pokemon } from "#field/pokemon"; -import type { ModifierConstructorMap } from "#modifiers/modifier"; -import type { ModifierType, WeightedModifierType } from "#modifiers/modifier-type"; -import type { ObjectValues } from "#types/type-helpers"; - -export type ModifierTypeFunc = () => ModifierType; -export type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number; - -export type { ModifierConstructorMap } from "#modifiers/modifier"; - -/** - * Map of modifier names to their respective instance types - */ -export type ModifierInstanceMap = { - [K in keyof ModifierConstructorMap]: InstanceType; -}; - -/** - * Union type of all modifier constructors. - */ -export type ModifierClass = ObjectValues; - -/** - * Union type of all modifier names as strings. - */ -export type ModifierString = keyof ModifierConstructorMap; - -export type ModifierPool = { - [tier: string]: WeightedModifierType[]; -}; diff --git a/src/@types/rewards.ts b/src/@types/rewards.ts new file mode 100644 index 00000000000..acf4749a3da --- /dev/null +++ b/src/@types/rewards.ts @@ -0,0 +1,32 @@ +import type { HeldItemId } from "#enums/held-item-id"; +import type { RewardId } from "#enums/reward-id"; +import type { TrainerItemId } from "#enums/trainer-item-id"; +import type { Pokemon } from "#field/pokemon"; +import type { Reward, RewardGenerator } from "#items/reward"; + +export type RewardFunc = () => Reward | RewardGenerator; +export type WeightedRewardWeightFunc = (party: Pokemon[], rerollCount?: number) => number; + +export type RewardPoolId = RewardId | HeldItemId | TrainerItemId; + +export type RewardGeneratorSpecs = { + id: RewardId; + args: RewardGeneratorArgs; +}; +// TODO: fix this with correctly typed args for different RewardIds + +export type RewardSpecs = RewardPoolId | RewardGeneratorSpecs; + +export type RewardPoolEntry = { + id: RewardPoolId; + weight: number | WeightedRewardWeightFunc; + maxWeight?: number; +}; + +export type RewardPool = { + [tier: string]: RewardPoolEntry[]; +}; + +export interface RewardPoolWeights { + [tier: string]: number[]; +} diff --git a/src/@types/trainer-funcs.ts b/src/@types/trainer-funcs.ts index aa839cbd158..a18b11c07f0 100644 --- a/src/@types/trainer-funcs.ts +++ b/src/@types/trainer-funcs.ts @@ -1,13 +1,13 @@ import type { PartyMemberStrength } from "#enums/party-member-strength"; import type { SpeciesId } from "#enums/species-id"; import type { EnemyPokemon } from "#field/pokemon"; -import type { PersistentModifier } from "#modifiers/modifier"; +import type { TrainerItemConfiguration } from "#items/trainer-item-data-types"; import type { TrainerConfig } from "#trainers/trainer-config"; import type { TrainerPartyTemplate } from "#trainers/trainer-party-template"; export type PartyTemplateFunc = () => TrainerPartyTemplate; export type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon; -export type GenModifiersFunc = (party: EnemyPokemon[]) => PersistentModifier[]; +export type GenTrainerItemsFunc = (party: EnemyPokemon[]) => TrainerItemConfiguration; export type GenAIFunc = (party: EnemyPokemon[]) => void; export interface TrainerTierPools { diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 8c8906be2b0..f0a544aa6d7 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -34,7 +34,7 @@ import { loadMoveAnimAssets, populateAnims, } from "#data/battle-anims"; -import { allAbilities, allMoves, allSpecies, modifierTypes } from "#data/data-lists"; +import { allAbilities, allHeldItems, allMoves, allSpecies, allTrainerItems } from "#data/data-lists"; import { battleSpecDialogue } from "#data/dialogue"; import type { SpeciesFormChangeTrigger } from "#data/form-change-triggers"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger } from "#data/form-change-triggers"; @@ -53,7 +53,8 @@ import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { ExpNotification } from "#enums/exp-notification"; import { FormChangeItem } from "#enums/form-change-item"; import { GameModes } from "#enums/game-modes"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; import { MoneyFormat } from "#enums/money-format"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; @@ -64,9 +65,11 @@ import { PlayerGender } from "#enums/player-gender"; import { PokeballType } from "#enums/pokeball"; import type { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonType } from "#enums/pokemon-type"; +import { HeldItemPoolType, RewardPoolType } from "#enums/reward-pool-type"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { TextStyle } from "#enums/text-style"; import type { TrainerSlot } from "#enums/trainer-slot"; import { TrainerType } from "#enums/trainer-type"; @@ -79,33 +82,21 @@ import type { Pokemon } from "#field/pokemon"; import { EnemyPokemon, PlayerPokemon } from "#field/pokemon"; import { PokemonSpriteSparkleHandler } from "#field/pokemon-sprite-sparkle-handler"; import { Trainer } from "#field/trainer"; -import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; +import { type ApplyTrainerItemsParams, applyTrainerItems } from "#items/apply-trainer-items"; +import type { HeldItemConfiguration } from "#items/held-item-data-types"; +import { assignEnemyHeldItemsForWave, assignItemsFromConfiguration } from "#items/held-item-pool"; +import type { Reward } from "#items/reward"; +import { getRewardPoolForType } from "#items/reward-pool-utils"; +import { type EnemyAttackStatusEffectChanceTrainerItem, TrainerItemEffect } from "#items/trainer-item"; import { - ConsumableModifier, - ConsumablePokemonModifier, - DoubleBattleChanceBoosterModifier, - ExpBalanceModifier, - ExpShareModifier, - FusePokemonModifier, - HealingBoosterModifier, - ModifierBar, - MultipleParticipantExpBonusModifier, - PersistentModifier, - PokemonExpBoosterModifier, - PokemonFormChangeItemModifier, - PokemonHeldItemModifier, - PokemonHpRestoreModifier, - PokemonIncrementingStatModifier, - RememberMoveModifier, -} from "#modifiers/modifier"; -import { - getDefaultModifierTypeForTier, - getEnemyModifierTypesForWave, - getLuckString, - getLuckTextTint, - getPartyLuckValue, - PokemonHeldItemModifierType, -} from "#modifiers/modifier-type"; + isTrainerItemPool, + isTrainerItemSpecs, + type TrainerItemConfiguration, + type TrainerItemSaveData, +} from "#items/trainer-item-data-types"; +import { TrainerItemManager } from "#items/trainer-item-manager"; +import { getNewTrainerItemFromPool } from "#items/trainer-item-pool"; import { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters"; @@ -115,7 +106,7 @@ import { hasExpSprite } from "#sprites/sprite-utils"; import type { Variant } from "#sprites/variant"; import { clearVariantData, variantData } from "#sprites/variant"; import type { Achv } from "#system/achv"; -import { achvs, ModifierAchv, MoneyAchv } from "#system/achv"; +import { achvs, HeldItemAchv, MoneyAchv } from "#system/achv"; import { GameData } from "#system/game-data"; import { initGameSpeed } from "#system/game-speed"; import type { PokemonData } from "#system/pokemon-data"; @@ -124,12 +115,12 @@ import type { TrainerData } from "#system/trainer-data"; import type { Voucher } from "#system/voucher"; import { vouchers } from "#system/voucher"; import { trainerConfigs } from "#trainers/trainer-config"; -import type { HeldModifierConfig } from "#types/held-modifier-config"; import type { Localizable } from "#types/locales"; import { AbilityBar } from "#ui/ability-bar"; import { ArenaFlyout } from "#ui/arena-flyout"; import { CandyBar } from "#ui/candy-bar"; import { CharSprite } from "#ui/char-sprite"; +import { ItemBar } from "#ui/item-bar-ui"; import { PartyExpBar } from "#ui/party-exp-bar"; import { PokeballTray } from "#ui/pokeball-tray"; import { PokemonInfoContainer } from "#ui/pokemon-info-container"; @@ -151,7 +142,7 @@ import { } from "#utils/common"; import { deepMergeSpriteData } from "#utils/data"; import { getEnumValues } from "#utils/enums"; -import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils"; +import { getLuckString, getLuckTextTint, getPartyLuckValue } from "#utils/party"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; import Phaser from "phaser"; @@ -271,7 +262,7 @@ export class BattleScene extends SceneBase { public arena: Arena; public gameMode: GameMode; public score: number; - public lockModifierTiers: boolean; + public lockRarityTiers: boolean; public trainer: Phaser.GameObjects.Sprite; public lastEnemyTrainer: Trainer | null; public currentBattle: Battle; @@ -289,8 +280,8 @@ export class BattleScene extends SceneBase { private scoreText: Phaser.GameObjects.Text; private luckLabelText: Phaser.GameObjects.Text; private luckText: Phaser.GameObjects.Text; - private modifierBar: ModifierBar; - private enemyModifierBar: ModifierBar; + private itemBar: ItemBar; + private enemyItemBar: ItemBar; public arenaFlyout: ArenaFlyout; private fieldOverlay: Phaser.GameObjects.Rectangle; @@ -298,8 +289,8 @@ export class BattleScene extends SceneBase { private shopOverlayShown = false; private shopOverlayOpacity = 0.8; - public modifiers: PersistentModifier[]; - private enemyModifiers: PersistentModifier[]; + public trainerItems: TrainerItemManager; + public enemyTrainerItems: TrainerItemManager; public uiContainer: Phaser.GameObjects.Container; public ui: UI; @@ -492,18 +483,18 @@ export class BattleScene extends SceneBase { this.shopOverlay.setAlpha(0); this.fieldUI.add(this.shopOverlay); - this.modifiers = []; - this.enemyModifiers = []; + this.trainerItems = new TrainerItemManager(); + this.enemyTrainerItems = new TrainerItemManager(); - this.modifierBar = new ModifierBar(); - this.modifierBar.setName("modifier-bar"); - this.add.existing(this.modifierBar); - uiContainer.add(this.modifierBar); + this.itemBar = new ItemBar(); + this.itemBar.setName("item-bar"); + this.add.existing(this.itemBar); + uiContainer.add(this.itemBar); - this.enemyModifierBar = new ModifierBar(true); - this.enemyModifierBar.setName("enemy-modifier-bar"); - this.add.existing(this.enemyModifierBar); - uiContainer.add(this.enemyModifierBar); + this.enemyItemBar = new ItemBar(true); + this.enemyItemBar.setName("enemy-item-bar"); + this.add.existing(this.enemyItemBar); + uiContainer.add(this.enemyItemBar); this.charSprite = new CharSprite(); this.charSprite.setName("sprite-char"); @@ -854,12 +845,12 @@ export class BattleScene extends SceneBase { } /** - * Returns the ModifierBar of this scene, which is declared private and therefore not accessible elsewhere + * Returns the ItemBar of this scene, which is declared private and therefore not accessible elsewhere * @param isEnemy - Whether to return the enemy modifier bar instead of the player bar; default `false` - * @returns The {@linkcode ModifierBar} for the given side of the field + * @returns The {@linkcode ItemBar} for the given side of the field */ - getModifierBar(isEnemy = false): ModifierBar { - return isEnemy ? this.enemyModifierBar : this.modifierBar; + getItemBar(isEnemy = false): ItemBar { + return isEnemy ? this.enemyItemBar : this.itemBar; } // store info toggles to be accessible by the ui @@ -897,6 +888,7 @@ export class BattleScene extends SceneBase { variant?: Variant, ivs?: number[], nature?: Nature, + heldItemConfig?: HeldItemConfiguration, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void, ): PlayerPokemon { @@ -910,6 +902,7 @@ export class BattleScene extends SceneBase { variant, ivs, nature, + heldItemConfig, dataSource, ); @@ -948,6 +941,7 @@ export class BattleScene extends SceneBase { trainerSlot: TrainerSlot, boss = false, shinyLock = false, + heldItemConfig?: HeldItemConfiguration, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void, ): EnemyPokemon { @@ -960,7 +954,7 @@ export class BattleScene extends SceneBase { boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1; } - const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, dataSource); + const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, heldItemConfig, dataSource); if (Overrides.OPP_FUSION_OVERRIDE) { pokemon.generateFusionSpecies(); } @@ -1004,6 +998,7 @@ export class BattleScene extends SceneBase { } pokemon.init(); + return pokemon; } @@ -1024,7 +1019,7 @@ export class BattleScene extends SceneBase { this.field.remove(pokemon, true); pokemon.destroy(); } - this.updateModifiers(true); + this.updateItems(true); } addPokemonIcon( @@ -1163,7 +1158,7 @@ export class BattleScene extends SceneBase { this.score = 0; this.money = 0; - this.lockModifierTiers = false; + this.lockRarityTiers = false; this.pokeballCounts = Object.fromEntries( getEnumValues(PokeballType) @@ -1175,10 +1170,10 @@ export class BattleScene extends SceneBase { this.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; } - this.modifiers = []; - this.enemyModifiers = []; - this.modifierBar.removeAll(true); - this.enemyModifierBar.removeAll(true); + this.trainerItems.clearItems(); + this.enemyTrainerItems.clearItems(); + this.itemBar.removeAll(true); + this.enemyItemBar.removeAll(true); for (const p of this.getPlayerParty()) { p.destroy(); @@ -1239,12 +1234,12 @@ export class BattleScene extends SceneBase { ...allSpecies, ...allMoves, ...allAbilities, - ...getEnumValues(ModifierPoolType) - .map(mpt => getModifierPoolForType(mpt)) + ...getEnumValues(RewardPoolType) + .map(mpt => getRewardPoolForType(mpt)) .flatMap(mp => Object.values(mp) .flat() - .map(mt => mt.modifierType) + .map(mt => mt.reward) .filter(mt => "localize" in mt) .map(lpb => lpb as unknown as Localizable), ), @@ -1279,7 +1274,7 @@ export class BattleScene extends SceneBase { getDoubleBattleChance(newWaveIndex: number, playerField: PlayerPokemon[]) { const doubleChance = new NumberHolder(newWaveIndex % 10 === 0 ? 32 : 8); - this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance); + this.applyPlayerItems(TrainerItemEffect.DOUBLE_BATTLE_CHANCE_BOOSTER, { numberHolder: doubleChance }); for (const p of playerField) { applyAbAttrs("DoubleBattleChanceAbAttr", { pokemon: p, chance: doubleChance }); } @@ -1987,12 +1982,12 @@ export class BattleScene extends SceneBase { }); } - showEnemyModifierBar(): void { - this.enemyModifierBar.setVisible(true); + showEnemyItemBar(): void { + this.enemyItemBar.setVisible(true); } - hideEnemyModifierBar(): void { - this.enemyModifierBar.setVisible(false); + hideEnemyItemBar(): void { + this.enemyItemBar.setVisible(false); } updateBiomeWaveText(): void { @@ -2085,11 +2080,11 @@ export class BattleScene extends SceneBase { } updateUIPositions(): void { - const enemyModifierCount = this.enemyModifiers.filter(m => m.isIconVisible()).length; + const enemyItemCount = this.enemyItemBar.totalVisibleLength; const biomeWaveTextHeight = this.biomeWaveText.getBottomLeft().y - this.biomeWaveText.getTopLeft().y; this.biomeWaveText.setY( -(this.game.canvas.height / 6) + - (enemyModifierCount ? (enemyModifierCount <= 12 ? 15 : 24) : 0) + + (enemyItemCount ? (enemyItemCount <= 12 ? 15 : 24) : 0) + biomeWaveTextHeight / 2, ); this.moneyText.setY(this.biomeWaveText.y + 10); @@ -2117,9 +2112,7 @@ export 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); } @@ -2639,127 +2632,47 @@ export class BattleScene extends SceneBase { return Math.floor(moneyValue / 10) * 10; } - addModifier( - modifier: Modifier | null, - ignoreUpdate?: boolean, - playSound?: boolean, - virtual?: boolean, - instant?: boolean, - cost?: number, - ): boolean { - // We check against modifier.type to stop a bug related to loading in a pokemon that has a form change item, which prior to some patch - // that changed form change modifiers worked, had previously set the `type` field to null. - // TODO: This is not the right place to check for this; it should ideally go in a session migrator. - if (!modifier || !modifier.type) { - return false; - } - let success = false; - const soundName = modifier.type.soundName; - this.validateAchvs(ModifierAchv, modifier); - const modifiersToRemove: PersistentModifier[] = []; - if (modifier instanceof PersistentModifier) { - if ((modifier as PersistentModifier).add(this.modifiers, !!virtual)) { - if (modifier instanceof PokemonFormChangeItemModifier) { - const pokemon = this.getPokemonById(modifier.pokemonId); - if (pokemon) { - success = modifier.apply(pokemon, true); - } - } - if (playSound && !this.sound.get(soundName)) { - this.playSound(soundName); - } - } else if (!virtual) { - const defaultModifierType = getDefaultModifierTypeForTier(modifier.type.tier); - this.phaseManager.queueMessage( - i18next.t("battle:itemStackFull", { - fullItemName: modifier.type.name, - itemName: defaultModifierType.name, - }), - undefined, - false, - 3000, - ); - return this.addModifier(defaultModifierType.newModifier(), ignoreUpdate, playSound, false, instant); - } - - for (const rm of modifiersToRemove) { - this.removeModifier(rm); - } - - if (!ignoreUpdate && !virtual) { - this.updateModifiers(true, instant); - } - } else if (modifier instanceof ConsumableModifier) { - if (playSound && !this.sound.get(soundName)) { - this.playSound(soundName); - } - - if (modifier instanceof ConsumablePokemonModifier) { - for (const p in this.party) { - const pokemon = this.party[p]; - - const args: unknown[] = []; - if (modifier instanceof PokemonHpRestoreModifier) { - if (!(modifier as PokemonHpRestoreModifier).fainted) { - const hpRestoreMultiplier = new NumberHolder(1); - this.applyModifiers(HealingBoosterModifier, true, hpRestoreMultiplier); - args.push(hpRestoreMultiplier.value); - } else { - args.push(1); - } - } else if (modifier instanceof FusePokemonModifier) { - args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon); - } else if (modifier instanceof RememberMoveModifier && !isNullOrUndefined(cost)) { - args.push(cost); - } - - if (modifier.shouldApply(pokemon, ...args)) { - const result = modifier.apply(pokemon, ...args); - success ||= result; - } - } - - this.party.map(p => p.updateInfo(instant)); - } else { - const args = [this]; - if (modifier.shouldApply(...args)) { - const result = modifier.apply(...args); - success ||= result; - } - } - } - return success; + applyPlayerItems(effect: T, params: ApplyTrainerItemsParams[T]) { + applyTrainerItems(effect, this.trainerItems, params); } - 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(); - } - }); + applyReward(reward: T, params: Parameters[0], playSound?: boolean): boolean { + const soundName = reward.soundName; + + if (playSound && !this.sound.get(soundName)) { + this.playSound(soundName); + } + + if (!reward.shouldApply(params)) { + return false; + } + + reward.apply(params); + return true; + } + + addHeldItem(heldItemId: HeldItemId, pokemon: Pokemon, amount = 1, playSound?: boolean, ignoreUpdate?: boolean) { + pokemon.heldItemManager.add(heldItemId, amount); + if (!ignoreUpdate) { + this.updateItems(pokemon.isPlayer()); + } + const soundName = allHeldItems[heldItemId].soundName; + if (playSound && !this.sound.get(soundName)) { + this.playSound(soundName); + } + + if (pokemon.isPlayer()) { + this.validateAchvs(HeldItemAchv, pokemon); + } } /** - * Try to transfer a held item to another pokemon. + * Try to transfer a held item from source to target. * If the recepient already has the maximum amount allowed for this item, the transfer is cancelled. * The quantity to transfer is automatically capped at how much the recepient can take before reaching the maximum stack size for the item. * A transfer that moves a quantity smaller than what is specified in the transferQuantity parameter is still considered successful. - * @param itemModifier {@linkcode PokemonHeldItemModifier} item to transfer (represents the whole stack) + * @param heldItemId {@linkcode HeldItemId} item to transfer + * @param source {@linkcode Pokemon} giver in this transfer * @param target {@linkcode Pokemon} recepient in this transfer * @param playSound `true` to play a sound when transferring the item * @param transferQuantity How many items of the stack to transfer. Optional, defaults to `1` @@ -2768,16 +2681,15 @@ export 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()) { @@ -2788,60 +2700,104 @@ export class BattleScene extends SceneBase { return false; } - const newItemModifier = itemModifier.clone() as PokemonHeldItemModifier; - newItemModifier.pokemonId = target.id; - const matchingModifier = this.findModifier( - m => m instanceof PokemonHeldItemModifier && m.matchType(itemModifier) && m.pokemonId === target.id, - target.isPlayer(), - ) as PokemonHeldItemModifier; + const itemStack = source.heldItemManager.getStack(heldItemId); + const matchingItemStack = target.heldItemManager.getStack(heldItemId); - if (matchingModifier) { - const maxStackCount = matchingModifier.getMaxStackCount(); - if (matchingModifier.stackCount >= maxStackCount) { - return false; - } - const countTaken = Math.min( - transferQuantity, - itemModifier.stackCount, - maxStackCount - matchingModifier.stackCount, - ); - itemModifier.stackCount -= countTaken; - newItemModifier.stackCount = matchingModifier.stackCount + countTaken; - } else { - const countTaken = Math.min(transferQuantity, itemModifier.stackCount); - itemModifier.stackCount -= countTaken; - newItemModifier.stackCount = countTaken; + const maxStackCount = allHeldItems[heldItemId].getMaxStackCount(); + if (matchingItemStack >= maxStackCount) { + return false; + } + const countTaken = Math.min(transferQuantity, itemStack, maxStackCount - matchingItemStack); + + const itemSpecs = source.heldItemManager.getItemSpecs(heldItemId); + if (!itemSpecs) { + return false; + } + source.heldItemManager.remove(heldItemId, countTaken); + target.heldItemManager.add(itemSpecs); + + if (source.heldItemManager.getStack(heldItemId) === 0 && itemLost) { + applyAbAttrs("PostItemLostAbAttr", { pokemon: source }); } - const removeOld = itemModifier.stackCount === 0; + if (source.isPlayer() !== target.isPlayer() && !ignoreUpdate) { + this.updateItems(source.isPlayer()); + } - if (!removeOld || !source || this.removeModifier(itemModifier, source.isEnemy())) { - const addModifier = () => { - if (!matchingModifier || this.removeModifier(matchingModifier, target.isEnemy())) { - if (target.isPlayer()) { - this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant); - if (source && itemLost) { - applyAbAttrs("PostItemLostAbAttr", { pokemon: source }); - } - return true; - } - this.addEnemyModifier(newItemModifier, ignoreUpdate, instant); - if (source && itemLost) { - applyAbAttrs("PostItemLostAbAttr", { pokemon: source }); - } - return true; - } - return false; - }; - if (source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) { - this.updateModifiers(source.isPlayer(), instant); - addModifier(); - } else { - addModifier(); + const soundName = allHeldItems[heldItemId].soundName; + if (playSound && !this.sound.get(soundName)) { + this.playSound(soundName); + } + + return true; + } + + canTransferHeldItem(heldItemId: HeldItemId, source: Pokemon, target: Pokemon, transferQuantity = 1): boolean { + const cancelled = new BooleanHolder(false); + + if (source && source.isPlayer() !== target.isPlayer()) { + applyAbAttrs("BlockItemTheftAbAttr", { pokemon: source, cancelled }); + } + + if (cancelled.value) { + return false; + } + + const itemStack = source.heldItemManager.getStack(heldItemId); + const matchingItemStack = target.heldItemManager.getStack(heldItemId); + + const maxStackCount = allHeldItems[heldItemId].getMaxStackCount(); + if (matchingItemStack >= maxStackCount) { + return false; + } + const countTaken = Math.min(transferQuantity, itemStack, maxStackCount - matchingItemStack); + + return countTaken > 0; + } + + assignTrainerItemsFromConfiguration(config: TrainerItemConfiguration, isPlayer: boolean) { + const manager = isPlayer ? this.trainerItems : this.enemyTrainerItems; + config.forEach(item => { + const { entry, count } = item; + const actualCount = typeof count === "function" ? count() : count; + + if (typeof entry === "number") { + manager.add(entry, actualCount); } + + if (isTrainerItemSpecs(entry)) { + manager.add(entry); + } + + if (isTrainerItemPool(entry)) { + for (let i = 1; i <= (actualCount ?? 1); i++) { + const newItem = getNewTrainerItemFromPool(entry, manager); + if (newItem) { + manager.add(newItem); + } + } + } + }); + } + + + // TODO @Wlowscha: Fix this + /** + * Attempt to discard one or more copies of a held item. + * @param itemModifier - The {@linkcode PokemonHeldItemModifier} being discarded + * @param discardQuantity - The number of copies to remove (up to the amount currently held); default `1` + * @returns Whether the item was successfully discarded. + * Removing fewer items than requested is still considered a success. + */ + tryDiscardHeldItemModifier(itemModifier: PokemonHeldItemModifier, discardQuantity = 1): boolean { + const countTaken = Math.min(discardQuantity, itemModifier.stackCount); + itemModifier.stackCount -= countTaken; + + if (itemModifier.stackCount > 0) { return true; } - return false; + + return this.removeModifier(itemModifier); } /** * Attempt to discard one or more copies of a held item. @@ -2910,7 +2866,14 @@ export class BattleScene extends SceneBase { }); } - generateEnemyModifiers(heldModifiersConfigs?: HeldModifierConfig[][]): Promise { + assignTrainerItemsFromSaveData(saveData: TrainerItemSaveData, isPlayer: boolean) { + const manager = isPlayer ? this.trainerItems : this.enemyTrainerItems; + for (const item of saveData) { + manager.add(item); + } + } + + generateEnemyItems(heldItemConfigs?: HeldItemConfiguration[]): Promise { return new Promise(resolve => { if (this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { return resolve(); @@ -2925,26 +2888,13 @@ export class BattleScene extends SceneBase { const party = this.getEnemyParty(); if (this.currentBattle.trainer) { - const modifiers = this.currentBattle.trainer.genModifiers(party); - for (const modifier of modifiers) { - this.addEnemyModifier(modifier, true, true); - } + const trainerItemConfig = this.currentBattle.trainer.genTrainerItems(party); + this.assignTrainerItemsFromConfiguration(trainerItemConfig, false); } party.forEach((enemyPokemon: EnemyPokemon, i: number) => { - if (heldModifiersConfigs && i < heldModifiersConfigs.length && heldModifiersConfigs[i]) { - for (const mt of heldModifiersConfigs[i]) { - let modifier: PokemonHeldItemModifier; - if (mt.modifier instanceof PokemonHeldItemModifierType) { - modifier = mt.modifier.newModifier(enemyPokemon); - } else { - modifier = mt.modifier as PokemonHeldItemModifier; - modifier.pokemonId = enemyPokemon.id; - } - modifier.stackCount = mt.stackCount ?? 1; - modifier.isTransferable = mt.isTransferable ?? modifier.isTransferable; - this.addEnemyModifier(modifier, true); - } + if (heldItemConfigs && i < heldItemConfigs.length && heldItemConfigs[i]) { + assignItemsFromConfiguration(heldItemConfigs[i], enemyPokemon); } else { const isBoss = enemyPokemon.isBoss() || @@ -2958,99 +2908,79 @@ export class BattleScene extends SceneBase { } let count = 0; for (let c = 0; c < chances; c++) { - if (!randSeedInt(this.gameMode.getEnemyModifierChance(isBoss))) { + if (!randSeedInt(this.gameMode.getEnemyItemChance(isBoss))) { count++; } } if (isBoss) { count = Math.max(count, Math.floor(chances / 2)); } - getEnemyModifierTypesForWave( + assignEnemyHeldItemsForWave( difficultyWaveIndex, count, - [enemyPokemon], - this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, + enemyPokemon, + this.currentBattle.battleType === BattleType.TRAINER ? HeldItemPoolType.TRAINER : HeldItemPoolType.WILD, upgradeChance, - ).map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false)); + ); } return true; }); - this.updateModifiers(false); + this.updateItems(false); resolve(); }); } /** - * Removes all modifiers from enemy pokemon of {@linkcode PersistentModifier} type + * Removes all items from enemy pokemon and trainers */ - clearEnemyModifiers(): void { - const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PersistentModifier); - for (const m of modifiersToRemove) { - this.enemyModifiers.splice(this.enemyModifiers.indexOf(m), 1); + clearEnemyItems(): void { + this.enemyTrainerItems.clearItems(); + for (const p of this.getEnemyParty()) { + p.heldItemManager.clearItems(); } - this.updateModifiers(false); + this.updateItems(false); this.updateUIPositions(); } - /** - * Removes all modifiers from enemy pokemon of {@linkcode PokemonHeldItemModifier} type - * @param pokemon - If specified, only removes held items from that {@linkcode Pokemon} - */ - clearEnemyHeldItemModifiers(pokemon?: Pokemon): void { - const modifiersToRemove = this.enemyModifiers.filter( - m => m instanceof PokemonHeldItemModifier && (!pokemon || m.getPokemon() === pokemon), - ); - for (const m of modifiersToRemove) { - this.enemyModifiers.splice(this.enemyModifiers.indexOf(m), 1); + clearAllItems(): void { + this.trainerItems.clearItems(); + this.enemyTrainerItems.clearItems(); + for (const p of this.getPlayerParty()) { + p.heldItemManager.clearItems(); } - this.updateModifiers(false); + for (const p of this.getEnemyParty()) { + p.heldItemManager.clearItems(); + } + this.updateItems(false); this.updateUIPositions(); } - setModifiersVisible(visible: boolean) { - [this.modifierBar, this.enemyModifierBar].map(m => m.setVisible(visible)); + setItemsVisible(visible: boolean) { + [this.itemBar, this.enemyItemBar].map(m => m.setVisible(visible)); } // TODO: Document this - updateModifiers(player = true, instant?: boolean): void { - const modifiers = player ? this.modifiers : (this.enemyModifiers as PersistentModifier[]); - for (let m = 0; m < modifiers.length; m++) { - const modifier = modifiers[m]; - if ( - modifier instanceof PokemonHeldItemModifier && - !this.getPokemonById((modifier as PokemonHeldItemModifier).pokemonId) - ) { - modifiers.splice(m--, 1); - } - if ( - modifier instanceof PokemonHeldItemModifier && - !isNullOrUndefined(modifier.getSpecies()) && - !this.getPokemonById(modifier.pokemonId)?.hasSpecies(modifier.getSpecies()!) - ) { - modifiers.splice(m--, 1); - } - } - for (const modifier of modifiers) { - if (modifier instanceof PersistentModifier) { - (modifier as PersistentModifier).virtualStackCount = 0; - } + updateItems(player = true, showHeldItems = true): void { + const trainerItems = player ? this.trainerItems : this.enemyTrainerItems; + + this.updateParty(player ? this.getPlayerParty() : this.getEnemyParty(), true); + + const pokemonA = player ? this.getPlayerParty()[0] : this.getEnemyParty()[0]; + + const bar = player ? this.itemBar : this.enemyItemBar; + + if (showHeldItems) { + bar.updateItems(trainerItems, pokemonA); + } else { + bar.updateItems(trainerItems); } - const modifiersClone = modifiers.slice(0); - for (const modifier of modifiersClone) { - if (!modifier.getStackCount()) { - modifiers.splice(modifiers.indexOf(modifier), 1); - } - } - - this.updatePartyForModifiers(player ? this.getPlayerParty() : this.getEnemyParty(), instant); - (player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers); if (!player) { this.updateUIPositions(); } } - updatePartyForModifiers(party: Pokemon[], instant?: boolean): Promise { + updateParty(party: Pokemon[], instant?: boolean): Promise { return new Promise(resolve => { Promise.allSettled( party.map(p => { @@ -3061,156 +2991,33 @@ export class BattleScene extends SceneBase { }); } - hasModifier(modifier: PersistentModifier, enemy = false): boolean { - const modifiers = !enemy ? this.modifiers : this.enemyModifiers; - return modifiers.indexOf(modifier) > -1; - } + applyShuffledStatusTokens(pokemon: Pokemon) { + let tokens = [ + TrainerItemId.ENEMY_ATTACK_BURN_CHANCE, + TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE, + TrainerItemId.ENEMY_ATTACK_POISON_CHANCE, + ].filter(t => this.enemyTrainerItems.hasItem(t)); - /** - * Removes a currently owned item. If the item is stacked, the entire item stack - * gets removed. This function does NOT apply in-battle effects, such as Unburden. - * If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead. - * @param modifier The item to be removed. - * @param enemy `true` to remove an item owned by the enemy rather than the player; default `false`. - * @returns `true` if the item exists and was successfully removed, `false` otherwise - */ - removeModifier(modifier: PersistentModifier, enemy = false): boolean { - const modifiers = !enemy ? this.modifiers : this.enemyModifiers; - const modifierIndex = modifiers.indexOf(modifier); - if (modifierIndex > -1) { - modifiers.splice(modifierIndex, 1); - if (modifier instanceof PokemonFormChangeItemModifier) { - const pokemon = this.getPokemonById(modifier.pokemonId); - if (pokemon) { - modifier.apply(pokemon, false); - } - } - return true; - } - - return false; - } - - /** - * Get all of the modifiers that match `modifierType` - * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @returns the list of all modifiers that matched `modifierType`. - */ - getModifiers(modifierType: Constructor, player = true): T[] { - return (player ? this.modifiers : this.enemyModifiers).filter((m): m is T => m instanceof modifierType); - } - - /** - * Get all of the modifiers that pass the `modifierFilter` function - * @param modifierFilter The function used to filter a target's modifiers - * @param isPlayer Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @returns the list of all modifiers that passed the `modifierFilter` function - */ - findModifiers(modifierFilter: ModifierPredicate, isPlayer = true): PersistentModifier[] { - return (isPlayer ? this.modifiers : this.enemyModifiers).filter(modifierFilter); - } - - /** - * Find the first modifier that pass the `modifierFilter` function - * @param modifierFilter The function used to filter a target's modifiers - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @returns the first modifier that passed the `modifierFilter` function; `undefined` if none passed - */ - findModifier(modifierFilter: ModifierPredicate, player = true): PersistentModifier | undefined { - return (player ? this.modifiers : this.enemyModifiers).find(modifierFilter); - } - - /** - * Apply all modifiers that match `modifierType` in a random order - * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` - * @returns the list of all modifiers that matched `modifierType` and were applied. - */ - applyShuffledModifiers( - modifierType: Constructor, - player = true, - ...args: Parameters - ): T[] { - let modifiers = (player ? this.modifiers : this.enemyModifiers).filter( - (m): m is T => m instanceof modifierType && m.shouldApply(...args), - ); this.executeWithSeedOffset( () => { - const shuffleModifiers = mods => { - if (mods.length < 1) { - return mods; + const shuffleTokens = toks => { + if (toks.length < 1) { + return toks; } - const rand = randSeedInt(mods.length); - return [mods[rand], ...shuffleModifiers(mods.filter((_, i) => i !== rand))]; + const rand = randSeedInt(toks.length); + return [toks[rand], ...shuffleTokens(toks.filter((_, i) => i !== rand))]; }; - modifiers = shuffleModifiers(modifiers); + tokens = shuffleTokens(tokens); }, this.currentBattle.turn << 4, this.waveSeed, ); - return this.applyModifiersInternal(modifiers, player, args); - } - /** - * Apply all modifiers that match `modifierType` - * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` - * @returns the list of all modifiers that matched `modifierType` and were applied. - */ - applyModifiers( - modifierType: Constructor, - player = true, - ...args: Parameters - ): T[] { - const modifiers = (player ? this.modifiers : this.enemyModifiers).filter( - (m): m is T => m instanceof modifierType && m.shouldApply(...args), - ); - return this.applyModifiersInternal(modifiers, player, args); - } - - /** Helper function to apply all passed modifiers */ - applyModifiersInternal( - modifiers: T[], - player: boolean, - args: Parameters, - ): T[] { - const appliedModifiers: T[] = []; - for (const modifier of modifiers) { - if (modifier.apply(...args)) { - console.log("Applied", modifier.type.name, !player ? "(enemy)" : ""); - appliedModifiers.push(modifier); - } + for (const t in tokens) { + (allTrainerItems[t] as EnemyAttackStatusEffectChanceTrainerItem).apply(this.enemyTrainerItems, { + pokemon: pokemon, + }); } - - return appliedModifiers; - } - - /** - * Apply the first modifier that matches `modifierType` - * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} - * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` - * @returns the first modifier that matches `modifierType` and was applied; return `null` if none matched - */ - applyModifier( - modifierType: Constructor, - player = true, - ...args: Parameters - ): T | null { - const modifiers = (player ? this.modifiers : this.enemyModifiers).filter( - (m): m is T => m instanceof modifierType && m.shouldApply(...args), - ); - for (const modifier of modifiers) { - if (modifier.apply(...args)) { - console.log("Applied", modifier.type.name, !player ? "(enemy)" : ""); - return modifier; - } - } - - return null; } triggerPokemonFormChange( @@ -3227,22 +3034,17 @@ export 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) { @@ -3372,11 +3174,7 @@ export 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); @@ -3413,11 +3211,9 @@ export class BattleScene extends SceneBase { ): void { const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds; const party = this.getPlayerParty(); - const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; - const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; - const multipleParticipantExpBonusModifier = this.findModifier( - m => m instanceof MultipleParticipantExpBonusModifier, - ) as MultipleParticipantExpBonusModifier; + const expShareStack = this.trainerItems.getStack(TrainerItemId.EXP_SHARE); + const expBalanceStack = this.trainerItems.getStack(TrainerItemId.EXP_BALANCE); + const ovalCharmStack = this.trainerItems.getStack(TrainerItemId.OVAL_CHARM); const nonFaintedPartyMembers = party.filter(p => p.hp); const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.getMaxExpLevel()); const partyMemberExp: number[] = []; @@ -3440,28 +3236,28 @@ export class BattleScene extends SceneBase { const participated = participantIds.has(pId); if (participated && pokemonDefeated) { partyMember.addFriendship(FRIENDSHIP_GAIN_FROM_BATTLE); - const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier); - if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount()) { - machoBraceModifier.stackCount++; - this.updateModifiers(true, true); + const hasMachoBrace = partyMember.heldItemManager.hasItem(HeldItemId.MACHO_BRACE); + if (hasMachoBrace) { + partyMember.heldItemManager.add(HeldItemId.MACHO_BRACE); + this.updateItems(true); partyMember.updateInfo(); } } if (!expPartyMembers.includes(partyMember)) { continue; } - if (!participated && !expShareModifier) { + if (!participated && !expShareStack) { partyMemberExp.push(0); continue; } let expMultiplier = 0; if (participated) { expMultiplier += 1 / participantIds.size; - if (participantIds.size > 1 && multipleParticipantExpBonusModifier) { - expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; + if (participantIds.size > 1 && ovalCharmStack) { + expMultiplier += ovalCharmStack * 0.2; } - } else if (expShareModifier) { - expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size; + } else if (expShareStack) { + expMultiplier += (expShareStack * 0.2) / participantIds.size; } if (partyMember.pokerus) { expMultiplier *= 1.5; @@ -3470,11 +3266,11 @@ export class BattleScene extends SceneBase { expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; } const pokemonExp = new NumberHolder(expValue * expMultiplier); - this.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); + applyHeldItems(HeldItemEffect.EXP_BOOSTER, { pokemon: partyMember, expAmount: pokemonExp }); partyMemberExp.push(Math.floor(pokemonExp.value)); } - if (expBalanceModifier) { + if (expBalanceStack) { let totalLevel = 0; let totalExp = 0; expPartyMembers.forEach((expPartyMember, epm) => { @@ -3497,7 +3293,7 @@ export class BattleScene extends SceneBase { partyMemberExp[pm] = Phaser.Math.Linear( partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, - 0.2 * expBalanceModifier.getStackCount(), + 0.2 * expBalanceStack, ); }); } diff --git a/src/battle.ts b/src/battle.ts index 7b6a58cbaca..709e7a23164 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -5,6 +5,7 @@ import { BattleSpec } from "#enums/battle-spec"; import { BattleType } from "#enums/battle-type"; import { BattlerIndex } from "#enums/battler-index"; import type { Command } from "#enums/command"; +import type { HeldItemId } from "#enums/held-item-id"; import type { MoveId } from "#enums/move-id"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -15,8 +16,8 @@ import { TrainerType } from "#enums/trainer-type"; import { TrainerVariant } from "#enums/trainer-variant"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import { Trainer } from "#field/trainer"; -import { MoneyMultiplierModifier, type PokemonHeldItemModifier } from "#modifiers/modifier"; -import type { CustomModifierSettings } from "#modifiers/modifier-type"; +import type { CustomRewardSettings } from "#items/reward-pool-utils"; +import { TrainerItemEffect } from "#items/trainer-item"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import i18next from "#plugins/i18n"; import { MusicPreference } from "#system/settings"; @@ -68,7 +69,7 @@ export 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); @@ -173,24 +174,12 @@ export class Battle { } addPostBattleLoot(enemyPokemon: EnemyPokemon): void { - this.postBattleLoot.push( - ...globalScene - .findModifiers( - m => m.is("PokemonHeldItemModifier") && m.pokemonId === enemyPokemon.id && m.isTransferable, - false, - ) - .map(i => { - const ret = i as PokemonHeldItemModifier; - //@ts-expect-error - this is awful to fix/change - ret.pokemonId = null; - return ret; - }), - ); + this.postBattleLoot.push(...enemyPokemon.getHeldItems()); } pickUpScatteredMoney(): void { const moneyAmount = new NumberHolder(globalScene.currentBattle.moneyScattered); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); + globalScene.applyPlayerItems(TrainerItemEffect.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) { moneyAmount.value *= 2; @@ -491,7 +480,7 @@ export class FixedBattleConfig { public getTrainer: GetTrainerFunc; public getEnemyParty: GetEnemyPartyFunc; public seedOffsetWaveIndex: number; - public customModifierRewardSettings?: CustomModifierSettings; + public customRewardSettings?: CustomRewardSettings; setBattleType(battleType: BattleType): FixedBattleConfig { this.battleType = battleType; @@ -518,8 +507,8 @@ export class FixedBattleConfig { return this; } - setCustomModifierRewards(customModifierRewardSettings: CustomModifierSettings) { - this.customModifierRewardSettings = customModifierRewardSettings; + setCustomRewards(customRewardSettings: CustomRewardSettings) { + this.customRewardSettings = customRewardSettings; return this; } } diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 0ee1a51a78e..42a63a65727 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -10,7 +10,7 @@ import type { ArenaTrapTag, SuppressAbilitiesTag } from "#data/arena-tag"; import type { BattlerTag } from "#data/battler-tags"; import { GroundedTag } from "#data/battler-tags"; import { getBerryEffectFunc } from "#data/berry"; -import { allAbilities, allMoves } from "#data/data-lists"; +import { allAbilities, allHeldItems, allMoves } from "#data/data-lists"; import { SpeciesFormChangeAbilityTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers"; import { Gender } from "#data/gender"; import { getPokeballName } from "#data/pokeball"; @@ -28,6 +28,7 @@ import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import type { BerryType } from "#enums/berry-type"; import { Command } from "#enums/command"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; import { HitResult } from "#enums/hit-result"; import { CommonAnim } from "#enums/move-anims-common"; import { MoveCategory } from "#enums/move-category"; @@ -46,8 +47,7 @@ import { SwitchType } from "#enums/switch-type"; import { WeatherType } from "#enums/weather-type"; import { BerryUsedEvent } from "#events/battle-scene"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; -import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; -import { BerryModifierType } from "#modifiers/modifier-type"; +import { type BerryHeldItem, berryTypeToHeldItem } from "#items/berry"; import { applyMoveAttrs } from "#moves/apply-attrs"; import { noAbilityTypeOverrideMoves } from "#moves/invalid-moves"; import type { Move } from "#moves/move"; @@ -2137,7 +2137,7 @@ export abstract class PostAttackAbAttr extends AbAttr { export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { private stealCondition: PokemonAttackCondition | null; - private stolenItem?: PokemonHeldItemModifier; + private stolenItem?: HeldItemId; constructor(stealCondition?: PokemonAttackCondition) { super(); @@ -2156,11 +2156,11 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { hitResult < HitResult.NO_EFFECT && (!this.stealCondition || this.stealCondition(pokemon, opponent, move)) ) { - const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); + const heldItems = opponent.heldItemManager.getTransferableHeldItems(); if (heldItems.length) { // Ensure that the stolen item in testing is the same as when the effect is applied this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; - if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { + if (globalScene.canTransferHeldItem(this.stolenItem, opponent, pokemon)) { return true; } } @@ -2170,28 +2170,21 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { } override apply({ opponent, pokemon }: PostMoveInteractionAbAttrParams): void { - const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); + const heldItems = opponent.heldItemManager.getTransferableHeldItems(); if (!this.stolenItem) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; } - if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { + if (globalScene.tryTransferHeldItem(this.stolenItem, opponent, pokemon, false)) { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postAttackStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), defenderName: opponent.name, - stolenItemType: this.stolenItem.type.name, + stolenItemType: allHeldItems[this.stolenItem].name, }), ); } this.stolenItem = undefined; } - - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === target.id, - target.isPlayer(), - ) as PokemonHeldItemModifier[]; - } } export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { @@ -2282,7 +2275,7 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr { export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { private condition?: PokemonDefendCondition; - private stolenItem?: PokemonHeldItemModifier; + private stolenItem?: HeldItemId; constructor(condition?: PokemonDefendCondition) { super(); @@ -2292,10 +2285,10 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { override canApply({ simulated, pokemon, opponent, move, hitResult }: PostMoveInteractionAbAttrParams): boolean { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, opponent, move))) { - const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); + const heldItems = opponent.heldItemManager.getTransferableHeldItems(); if (heldItems.length) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; - if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { + if (globalScene.canTransferHeldItem(this.stolenItem, opponent, pokemon)) { return true; } } @@ -2304,28 +2297,21 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { } override apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void { - const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); + const heldItems = opponent.heldItemManager.getTransferableHeldItems(); if (!this.stolenItem) { this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; } - if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { + if (globalScene.tryTransferHeldItem(this.stolenItem, opponent, pokemon, false)) { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postDefendStealHeldItem", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), attackerName: opponent.name, - stolenItemType: this.stolenItem.type.name, + stolenItemType: allHeldItems[this.stolenItem].name, }), ); } this.stolenItem = undefined; } - - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === target.id, - target.isPlayer(), - ) as PokemonHeldItemModifier[]; - } } /** @@ -4661,10 +4647,14 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { override canApply({ pokemon }: AbAttrBaseParams): boolean { // Ensure we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped) const cappedBerries = new Set( - globalScene - .getModifiers(BerryModifier, pokemon.isPlayer()) - .filter(bm => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1) - .map(bm => bm.berryType), + pokemon + .getHeldItems() + .filter( + bm => + isItemInCategory(bm, HeldItemCategoryId.BERRY) && + pokemon.heldItemManager.getStack(bm) < allHeldItems[bm].maxStackCount, + ) + .map(bm => (allHeldItems[bm] as BerryHeldItem).berryType), ); this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(bt => !cappedBerries.has(bt)); @@ -4694,30 +4684,15 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { const randomIdx = randSeedInt(this.berriesUnderCap.length); const chosenBerryType = this.berriesUnderCap[randomIdx]; pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory - const chosenBerry = new BerryModifierType(chosenBerryType); + const chosenBerry = berryTypeToHeldItem[chosenBerryType]; - // Add the randomly chosen berry or update the existing one - const berryModifier = globalScene.findModifier( - m => m instanceof BerryModifier && m.berryType === chosenBerryType && m.pokemonId === pokemon.id, - pokemon.isPlayer(), - ) as BerryModifier | undefined; + pokemon.heldItemManager.add(chosenBerry); - if (berryModifier) { - berryModifier.stackCount++; - } else { - const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1); - if (pokemon.isPlayer()) { - globalScene.addModifier(newBerry); - } else { - globalScene.addEnemyModifier(newBerry); - } - } - - globalScene.updateModifiers(pokemon.isPlayer()); + globalScene.updateItems(pokemon.isPlayer()); globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - berryName: chosenBerry.name, + berryName: allHeldItems[chosenBerry].name, }), ); return true; @@ -4751,8 +4726,7 @@ export class CudChewConsumeBerryAbAttr extends AbAttr { // This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed. for (const berryType of pokemon.summonData.berriesEatenLast) { getBerryEffectFunc(berryType)(pokemon); - const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1); - globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message + globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(pokemon, berryType)); // trigger message } // uncomment to make cheek pouch work with cud chew @@ -5343,13 +5317,13 @@ export abstract class PostBattleAbAttr extends AbAttr { } export class PostBattleLootAbAttr extends PostBattleAbAttr { - private randItem?: PokemonHeldItemModifier; + private randItem?: HeldItemId; override canApply({ simulated, victory, pokemon }: PostBattleAbAttrParams): boolean { const postBattleLoot = globalScene.currentBattle.postBattleLoot; if (!simulated && postBattleLoot.length && victory) { this.randItem = randSeedItem(postBattleLoot); - return globalScene.canTransferHeldItemModifier(this.randItem, pokemon, 1); + return pokemon.heldItemManager.getStack(this.randItem) < allHeldItems[this.randItem].maxStackCount; } return false; } @@ -5360,12 +5334,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, }), ); } @@ -6312,8 +6286,6 @@ class ForceSwitchOutHelper { } if (!allyPokemon?.isActive(true)) { - globalScene.clearEnemyHeldItemModifiers(); - if (switchOutTarget.hp) { globalScene.phaseManager.pushNew("BattleEndPhase", false); @@ -6397,11 +6369,9 @@ class ForceSwitchOutHelper { * @returns The amount of health recovered by Shell Bell. */ function calculateShellBellRecovery(pokemon: Pokemon): number { - const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); - if (shellBellModifier) { - return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount; - } - return 0; + // Returns 0 if no Shell Bell is present + const shellBellStack = pokemon.heldItemManager.getStack(HeldItemId.SHELL_BELL); + return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellStack; } export interface PostDamageAbAttrParams extends AbAttrBaseParams { diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index ab535682e86..aae3fd99b05 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -1,8 +1,9 @@ import { globalScene } from "#app/global-scene"; import { speciesStarterCosts } from "#balance/starters"; -import { allMoves } from "#data/data-lists"; +import { allHeldItems, allMoves } from "#data/data-lists"; import { Gender, getGenderSymbol } from "#data/gender"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { Nature } from "#enums/nature"; import { PokeballType } from "#enums/pokeball"; @@ -12,7 +13,6 @@ import { SpeciesId } from "#enums/species-id"; import { TimeOfDay } from "#enums/time-of-day"; import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; -import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type"; import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -99,7 +99,7 @@ const EvoCondKey = { SPECIES_CAUGHT: 12, GENDER: 13, NATURE: 14, - HELD_ITEM: 15, // Currently checks only for species stat booster items + HELD_ITEM: 15, } as const; type EvolutionConditionData = @@ -110,7 +110,7 @@ type EvolutionConditionData = {key: typeof EvoCondKey.GENDER, gender: Gender} | {key: typeof EvoCondKey.MOVE_TYPE | typeof EvoCondKey.PARTY_TYPE, pkmnType: PokemonType} | {key: typeof EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId} | - {key: typeof EvoCondKey.HELD_ITEM, itemKey: SpeciesStatBoosterItem} | + {key: typeof EvoCondKey.HELD_ITEM, itemKey: HeldItemId} | {key: typeof EvoCondKey.NATURE, nature: Nature[]} | {key: typeof EvoCondKey.WEATHER, weather: WeatherType[]} | {key: typeof EvoCondKey.TYROGUE, move: TyrogueMove} | @@ -177,10 +177,7 @@ export class SpeciesEvolutionCondition { case EvoCondKey.PARTY_TYPE: return globalScene.getPlayerParty().some(p => p.getTypes(false, false, true).includes(cond.pkmnType)) case EvoCondKey.EVO_TREASURE_TRACKER: - return pokemon.getHeldItems().some(m => - m.is("EvoTrackerModifier") && - m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value - ); + return allHeldItems[HeldItemId.GIMMIGHOUL_EVO_TRACKER].getStackCount(pokemon) >= cond.value; case EvoCondKey.GENDER: return pokemon.gender === cond.gender; case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly @@ -201,7 +198,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 +1762,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..d24bd28de1a 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -1,4 +1,4 @@ -import { ModifierTier } from "#enums/modifier-tier"; +import { RarityTier } 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]: RarityTier } 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]: RarityTier.GREAT, + [MoveId.PAY_DAY]: RarityTier.ULTRA, + [MoveId.FIRE_PUNCH]: RarityTier.GREAT, + [MoveId.ICE_PUNCH]: RarityTier.GREAT, + [MoveId.THUNDER_PUNCH]: RarityTier.GREAT, + [MoveId.SWORDS_DANCE]: RarityTier.COMMON, + [MoveId.CUT]: RarityTier.COMMON, + [MoveId.FLY]: RarityTier.COMMON, + [MoveId.MEGA_KICK]: RarityTier.GREAT, + [MoveId.BODY_SLAM]: RarityTier.GREAT, + [MoveId.TAKE_DOWN]: RarityTier.GREAT, + [MoveId.DOUBLE_EDGE]: RarityTier.ULTRA, + [MoveId.PIN_MISSILE]: RarityTier.COMMON, + [MoveId.ROAR]: RarityTier.COMMON, + [MoveId.FLAMETHROWER]: RarityTier.ULTRA, + [MoveId.HYDRO_PUMP]: RarityTier.ULTRA, + [MoveId.SURF]: RarityTier.ULTRA, + [MoveId.ICE_BEAM]: RarityTier.ULTRA, + [MoveId.BLIZZARD]: RarityTier.ULTRA, + [MoveId.PSYBEAM]: RarityTier.GREAT, + [MoveId.HYPER_BEAM]: RarityTier.ULTRA, + [MoveId.LOW_KICK]: RarityTier.COMMON, + [MoveId.COUNTER]: RarityTier.COMMON, + [MoveId.STRENGTH]: RarityTier.GREAT, + [MoveId.SOLAR_BEAM]: RarityTier.ULTRA, + [MoveId.FIRE_SPIN]: RarityTier.COMMON, + [MoveId.THUNDERBOLT]: RarityTier.ULTRA, + [MoveId.THUNDER_WAVE]: RarityTier.COMMON, + [MoveId.THUNDER]: RarityTier.ULTRA, + [MoveId.EARTHQUAKE]: RarityTier.ULTRA, + [MoveId.DIG]: RarityTier.GREAT, + [MoveId.TOXIC]: RarityTier.GREAT, + [MoveId.PSYCHIC]: RarityTier.ULTRA, + [MoveId.AGILITY]: RarityTier.COMMON, + [MoveId.NIGHT_SHADE]: RarityTier.COMMON, + [MoveId.SCREECH]: RarityTier.COMMON, + [MoveId.DOUBLE_TEAM]: RarityTier.COMMON, + [MoveId.CONFUSE_RAY]: RarityTier.COMMON, + [MoveId.LIGHT_SCREEN]: RarityTier.COMMON, + [MoveId.HAZE]: RarityTier.COMMON, + [MoveId.REFLECT]: RarityTier.COMMON, + [MoveId.FOCUS_ENERGY]: RarityTier.COMMON, + [MoveId.METRONOME]: RarityTier.COMMON, + [MoveId.SELF_DESTRUCT]: RarityTier.GREAT, + [MoveId.FIRE_BLAST]: RarityTier.ULTRA, + [MoveId.WATERFALL]: RarityTier.GREAT, + [MoveId.SWIFT]: RarityTier.COMMON, + [MoveId.AMNESIA]: RarityTier.COMMON, + [MoveId.DREAM_EATER]: RarityTier.GREAT, + [MoveId.LEECH_LIFE]: RarityTier.ULTRA, + [MoveId.FLASH]: RarityTier.COMMON, + [MoveId.EXPLOSION]: RarityTier.GREAT, + [MoveId.REST]: RarityTier.COMMON, + [MoveId.ROCK_SLIDE]: RarityTier.GREAT, + [MoveId.TRI_ATTACK]: RarityTier.ULTRA, + [MoveId.SUPER_FANG]: RarityTier.COMMON, + [MoveId.SUBSTITUTE]: RarityTier.COMMON, + [MoveId.THIEF]: RarityTier.GREAT, + [MoveId.SNORE]: RarityTier.COMMON, + [MoveId.CURSE]: RarityTier.COMMON, + [MoveId.REVERSAL]: RarityTier.COMMON, + [MoveId.SPITE]: RarityTier.COMMON, + [MoveId.PROTECT]: RarityTier.COMMON, + [MoveId.SCARY_FACE]: RarityTier.COMMON, + [MoveId.SLUDGE_BOMB]: RarityTier.GREAT, + [MoveId.MUD_SLAP]: RarityTier.COMMON, + [MoveId.SPIKES]: RarityTier.COMMON, + [MoveId.ICY_WIND]: RarityTier.GREAT, + [MoveId.OUTRAGE]: RarityTier.ULTRA, + [MoveId.SANDSTORM]: RarityTier.COMMON, + [MoveId.GIGA_DRAIN]: RarityTier.ULTRA, + [MoveId.ENDURE]: RarityTier.COMMON, + [MoveId.CHARM]: RarityTier.COMMON, + [MoveId.FALSE_SWIPE]: RarityTier.COMMON, + [MoveId.SWAGGER]: RarityTier.COMMON, + [MoveId.STEEL_WING]: RarityTier.GREAT, + [MoveId.ATTRACT]: RarityTier.COMMON, + [MoveId.SLEEP_TALK]: RarityTier.COMMON, + [MoveId.HEAL_BELL]: RarityTier.COMMON, + [MoveId.RETURN]: RarityTier.ULTRA, + [MoveId.FRUSTRATION]: RarityTier.COMMON, + [MoveId.SAFEGUARD]: RarityTier.COMMON, + [MoveId.PAIN_SPLIT]: RarityTier.COMMON, + [MoveId.MEGAHORN]: RarityTier.ULTRA, + [MoveId.BATON_PASS]: RarityTier.COMMON, + [MoveId.ENCORE]: RarityTier.COMMON, + [MoveId.IRON_TAIL]: RarityTier.GREAT, + [MoveId.METAL_CLAW]: RarityTier.COMMON, + [MoveId.SYNTHESIS]: RarityTier.GREAT, + [MoveId.HIDDEN_POWER]: RarityTier.GREAT, + [MoveId.RAIN_DANCE]: RarityTier.COMMON, + [MoveId.SUNNY_DAY]: RarityTier.COMMON, + [MoveId.CRUNCH]: RarityTier.GREAT, + [MoveId.PSYCH_UP]: RarityTier.COMMON, + [MoveId.SHADOW_BALL]: RarityTier.ULTRA, + [MoveId.FUTURE_SIGHT]: RarityTier.GREAT, + [MoveId.ROCK_SMASH]: RarityTier.COMMON, + [MoveId.WHIRLPOOL]: RarityTier.COMMON, + [MoveId.BEAT_UP]: RarityTier.COMMON, + [MoveId.UPROAR]: RarityTier.GREAT, + [MoveId.HEAT_WAVE]: RarityTier.ULTRA, + [MoveId.HAIL]: RarityTier.COMMON, + [MoveId.TORMENT]: RarityTier.COMMON, + [MoveId.WILL_O_WISP]: RarityTier.COMMON, + [MoveId.FACADE]: RarityTier.GREAT, + [MoveId.FOCUS_PUNCH]: RarityTier.COMMON, + [MoveId.NATURE_POWER]: RarityTier.COMMON, + [MoveId.CHARGE]: RarityTier.COMMON, + [MoveId.TAUNT]: RarityTier.COMMON, + [MoveId.HELPING_HAND]: RarityTier.COMMON, + [MoveId.TRICK]: RarityTier.COMMON, + [MoveId.SUPERPOWER]: RarityTier.ULTRA, + [MoveId.RECYCLE]: RarityTier.COMMON, + [MoveId.REVENGE]: RarityTier.GREAT, + [MoveId.BRICK_BREAK]: RarityTier.GREAT, + [MoveId.KNOCK_OFF]: RarityTier.GREAT, + [MoveId.ENDEAVOR]: RarityTier.COMMON, + [MoveId.SKILL_SWAP]: RarityTier.COMMON, + [MoveId.IMPRISON]: RarityTier.COMMON, + [MoveId.SECRET_POWER]: RarityTier.COMMON, + [MoveId.DIVE]: RarityTier.GREAT, + [MoveId.FEATHER_DANCE]: RarityTier.COMMON, + [MoveId.BLAZE_KICK]: RarityTier.GREAT, + [MoveId.HYPER_VOICE]: RarityTier.ULTRA, + [MoveId.BLAST_BURN]: RarityTier.ULTRA, + [MoveId.HYDRO_CANNON]: RarityTier.ULTRA, + [MoveId.WEATHER_BALL]: RarityTier.COMMON, + [MoveId.FAKE_TEARS]: RarityTier.COMMON, + [MoveId.AIR_CUTTER]: RarityTier.GREAT, + [MoveId.OVERHEAT]: RarityTier.ULTRA, + [MoveId.ROCK_TOMB]: RarityTier.GREAT, + [MoveId.METAL_SOUND]: RarityTier.COMMON, + [MoveId.COSMIC_POWER]: RarityTier.COMMON, + [MoveId.SIGNAL_BEAM]: RarityTier.GREAT, + [MoveId.SAND_TOMB]: RarityTier.COMMON, + [MoveId.MUDDY_WATER]: RarityTier.GREAT, + [MoveId.BULLET_SEED]: RarityTier.GREAT, + [MoveId.AERIAL_ACE]: RarityTier.GREAT, + [MoveId.ICICLE_SPEAR]: RarityTier.GREAT, + [MoveId.IRON_DEFENSE]: RarityTier.GREAT, + [MoveId.DRAGON_CLAW]: RarityTier.ULTRA, + [MoveId.FRENZY_PLANT]: RarityTier.ULTRA, + [MoveId.BULK_UP]: RarityTier.COMMON, + [MoveId.BOUNCE]: RarityTier.GREAT, + [MoveId.MUD_SHOT]: RarityTier.GREAT, + [MoveId.POISON_TAIL]: RarityTier.GREAT, + [MoveId.COVET]: RarityTier.GREAT, + [MoveId.MAGICAL_LEAF]: RarityTier.GREAT, + [MoveId.CALM_MIND]: RarityTier.GREAT, + [MoveId.LEAF_BLADE]: RarityTier.ULTRA, + [MoveId.DRAGON_DANCE]: RarityTier.GREAT, + [MoveId.ROCK_BLAST]: RarityTier.GREAT, + [MoveId.WATER_PULSE]: RarityTier.GREAT, + [MoveId.ROOST]: RarityTier.GREAT, + [MoveId.GRAVITY]: RarityTier.COMMON, + [MoveId.GYRO_BALL]: RarityTier.COMMON, + [MoveId.BRINE]: RarityTier.GREAT, + [MoveId.PLUCK]: RarityTier.GREAT, + [MoveId.TAILWIND]: RarityTier.GREAT, + [MoveId.U_TURN]: RarityTier.GREAT, + [MoveId.CLOSE_COMBAT]: RarityTier.ULTRA, + [MoveId.PAYBACK]: RarityTier.COMMON, + [MoveId.ASSURANCE]: RarityTier.COMMON, + [MoveId.EMBARGO]: RarityTier.COMMON, + [MoveId.FLING]: RarityTier.COMMON, + [MoveId.GASTRO_ACID]: RarityTier.GREAT, + [MoveId.POWER_SWAP]: RarityTier.COMMON, + [MoveId.GUARD_SWAP]: RarityTier.COMMON, + [MoveId.WORRY_SEED]: RarityTier.GREAT, + [MoveId.TOXIC_SPIKES]: RarityTier.GREAT, + [MoveId.FLARE_BLITZ]: RarityTier.ULTRA, + [MoveId.AURA_SPHERE]: RarityTier.GREAT, + [MoveId.ROCK_POLISH]: RarityTier.COMMON, + [MoveId.POISON_JAB]: RarityTier.GREAT, + [MoveId.DARK_PULSE]: RarityTier.GREAT, + [MoveId.AQUA_TAIL]: RarityTier.GREAT, + [MoveId.SEED_BOMB]: RarityTier.GREAT, + [MoveId.AIR_SLASH]: RarityTier.GREAT, + [MoveId.X_SCISSOR]: RarityTier.GREAT, + [MoveId.BUG_BUZZ]: RarityTier.GREAT, + [MoveId.DRAGON_PULSE]: RarityTier.GREAT, + [MoveId.POWER_GEM]: RarityTier.GREAT, + [MoveId.DRAIN_PUNCH]: RarityTier.GREAT, + [MoveId.VACUUM_WAVE]: RarityTier.COMMON, + [MoveId.FOCUS_BLAST]: RarityTier.GREAT, + [MoveId.ENERGY_BALL]: RarityTier.GREAT, + [MoveId.BRAVE_BIRD]: RarityTier.ULTRA, + [MoveId.EARTH_POWER]: RarityTier.ULTRA, + [MoveId.GIGA_IMPACT]: RarityTier.GREAT, + [MoveId.NASTY_PLOT]: RarityTier.COMMON, + [MoveId.AVALANCHE]: RarityTier.GREAT, + [MoveId.SHADOW_CLAW]: RarityTier.GREAT, + [MoveId.THUNDER_FANG]: RarityTier.GREAT, + [MoveId.ICE_FANG]: RarityTier.GREAT, + [MoveId.FIRE_FANG]: RarityTier.GREAT, + [MoveId.PSYCHO_CUT]: RarityTier.GREAT, + [MoveId.ZEN_HEADBUTT]: RarityTier.GREAT, + [MoveId.FLASH_CANNON]: RarityTier.GREAT, + [MoveId.ROCK_CLIMB]: RarityTier.GREAT, + [MoveId.DEFOG]: RarityTier.COMMON, + [MoveId.TRICK_ROOM]: RarityTier.COMMON, + [MoveId.DRACO_METEOR]: RarityTier.ULTRA, + [MoveId.LEAF_STORM]: RarityTier.ULTRA, + [MoveId.POWER_WHIP]: RarityTier.ULTRA, + [MoveId.CROSS_POISON]: RarityTier.GREAT, + [MoveId.GUNK_SHOT]: RarityTier.ULTRA, + [MoveId.IRON_HEAD]: RarityTier.GREAT, + [MoveId.STONE_EDGE]: RarityTier.ULTRA, + [MoveId.STEALTH_ROCK]: RarityTier.COMMON, + [MoveId.GRASS_KNOT]: RarityTier.ULTRA, + [MoveId.BUG_BITE]: RarityTier.GREAT, + [MoveId.CHARGE_BEAM]: RarityTier.GREAT, + [MoveId.HONE_CLAWS]: RarityTier.COMMON, + [MoveId.WONDER_ROOM]: RarityTier.COMMON, + [MoveId.PSYSHOCK]: RarityTier.GREAT, + [MoveId.VENOSHOCK]: RarityTier.GREAT, + [MoveId.MAGIC_ROOM]: RarityTier.COMMON, + [MoveId.SMACK_DOWN]: RarityTier.COMMON, + [MoveId.SLUDGE_WAVE]: RarityTier.GREAT, + [MoveId.HEAVY_SLAM]: RarityTier.GREAT, + [MoveId.ELECTRO_BALL]: RarityTier.GREAT, + [MoveId.FLAME_CHARGE]: RarityTier.GREAT, + [MoveId.LOW_SWEEP]: RarityTier.GREAT, + [MoveId.ACID_SPRAY]: RarityTier.COMMON, + [MoveId.FOUL_PLAY]: RarityTier.ULTRA, + [MoveId.ROUND]: RarityTier.COMMON, + [MoveId.ECHOED_VOICE]: RarityTier.COMMON, + [MoveId.STORED_POWER]: RarityTier.COMMON, + [MoveId.ALLY_SWITCH]: RarityTier.COMMON, + [MoveId.SCALD]: RarityTier.GREAT, + [MoveId.HEX]: RarityTier.GREAT, + [MoveId.SKY_DROP]: RarityTier.GREAT, + [MoveId.INCINERATE]: RarityTier.GREAT, + [MoveId.QUASH]: RarityTier.COMMON, + [MoveId.ACROBATICS]: RarityTier.GREAT, + [MoveId.RETALIATE]: RarityTier.GREAT, + [MoveId.WATER_PLEDGE]: RarityTier.GREAT, + [MoveId.FIRE_PLEDGE]: RarityTier.GREAT, + [MoveId.GRASS_PLEDGE]: RarityTier.GREAT, + [MoveId.VOLT_SWITCH]: RarityTier.GREAT, + [MoveId.STRUGGLE_BUG]: RarityTier.COMMON, + [MoveId.BULLDOZE]: RarityTier.GREAT, + [MoveId.FROST_BREATH]: RarityTier.GREAT, + [MoveId.DRAGON_TAIL]: RarityTier.GREAT, + [MoveId.WORK_UP]: RarityTier.COMMON, + [MoveId.ELECTROWEB]: RarityTier.GREAT, + [MoveId.WILD_CHARGE]: RarityTier.GREAT, + [MoveId.DRILL_RUN]: RarityTier.GREAT, + [MoveId.RAZOR_SHELL]: RarityTier.GREAT, + [MoveId.HEAT_CRASH]: RarityTier.GREAT, + [MoveId.TAIL_SLAP]: RarityTier.GREAT, + [MoveId.HURRICANE]: RarityTier.ULTRA, + [MoveId.SNARL]: RarityTier.COMMON, + [MoveId.PHANTOM_FORCE]: RarityTier.ULTRA, + [MoveId.PETAL_BLIZZARD]: RarityTier.GREAT, + [MoveId.DISARMING_VOICE]: RarityTier.GREAT, + [MoveId.DRAINING_KISS]: RarityTier.GREAT, + [MoveId.GRASSY_TERRAIN]: RarityTier.COMMON, + [MoveId.MISTY_TERRAIN]: RarityTier.COMMON, + [MoveId.PLAY_ROUGH]: RarityTier.GREAT, + [MoveId.CONFIDE]: RarityTier.COMMON, + [MoveId.MYSTICAL_FIRE]: RarityTier.GREAT, + [MoveId.EERIE_IMPULSE]: RarityTier.COMMON, + [MoveId.VENOM_DRENCH]: RarityTier.COMMON, + [MoveId.ELECTRIC_TERRAIN]: RarityTier.COMMON, + [MoveId.DAZZLING_GLEAM]: RarityTier.ULTRA, + [MoveId.INFESTATION]: RarityTier.COMMON, + [MoveId.POWER_UP_PUNCH]: RarityTier.GREAT, + [MoveId.DARKEST_LARIAT]: RarityTier.GREAT, + [MoveId.HIGH_HORSEPOWER]: RarityTier.ULTRA, + [MoveId.SOLAR_BLADE]: RarityTier.GREAT, + [MoveId.THROAT_CHOP]: RarityTier.GREAT, + [MoveId.POLLEN_PUFF]: RarityTier.GREAT, + [MoveId.PSYCHIC_TERRAIN]: RarityTier.COMMON, + [MoveId.LUNGE]: RarityTier.GREAT, + [MoveId.SPEED_SWAP]: RarityTier.COMMON, + [MoveId.SMART_STRIKE]: RarityTier.GREAT, + [MoveId.BRUTAL_SWING]: RarityTier.GREAT, + [MoveId.AURORA_VEIL]: RarityTier.COMMON, + [MoveId.PSYCHIC_FANGS]: RarityTier.GREAT, + [MoveId.STOMPING_TANTRUM]: RarityTier.GREAT, + [MoveId.LIQUIDATION]: RarityTier.ULTRA, + [MoveId.BODY_PRESS]: RarityTier.ULTRA, + [MoveId.BREAKING_SWIPE]: RarityTier.GREAT, + [MoveId.STEEL_BEAM]: RarityTier.ULTRA, + [MoveId.EXPANDING_FORCE]: RarityTier.GREAT, + [MoveId.STEEL_ROLLER]: RarityTier.COMMON, + [MoveId.SCALE_SHOT]: RarityTier.ULTRA, + [MoveId.METEOR_BEAM]: RarityTier.GREAT, + [MoveId.MISTY_EXPLOSION]: RarityTier.COMMON, + [MoveId.GRASSY_GLIDE]: RarityTier.COMMON, + [MoveId.RISING_VOLTAGE]: RarityTier.COMMON, + [MoveId.TERRAIN_PULSE]: RarityTier.COMMON, + [MoveId.SKITTER_SMACK]: RarityTier.GREAT, + [MoveId.BURNING_JEALOUSY]: RarityTier.GREAT, + [MoveId.LASH_OUT]: RarityTier.GREAT, + [MoveId.POLTERGEIST]: RarityTier.ULTRA, + [MoveId.CORROSIVE_GAS]: RarityTier.COMMON, + [MoveId.COACHING]: RarityTier.COMMON, + [MoveId.FLIP_TURN]: RarityTier.COMMON, + [MoveId.TRIPLE_AXEL]: RarityTier.COMMON, + [MoveId.DUAL_WINGBEAT]: RarityTier.COMMON, + [MoveId.SCORCHING_SANDS]: RarityTier.GREAT, + [MoveId.TERA_BLAST]: RarityTier.GREAT, + [MoveId.ICE_SPINNER]: RarityTier.GREAT, + [MoveId.SNOWSCAPE]: RarityTier.COMMON, + [MoveId.POUNCE]: RarityTier.COMMON, + [MoveId.TRAILBLAZE]: RarityTier.COMMON, + [MoveId.CHILLING_WATER]: RarityTier.COMMON, + [MoveId.HARD_PRESS]: RarityTier.GREAT, + [MoveId.DRAGON_CHEER]: RarityTier.COMMON, + [MoveId.ALLURING_VOICE]: RarityTier.GREAT, + [MoveId.TEMPER_FLARE]: RarityTier.GREAT, + [MoveId.SUPERCELL_SLAM]: RarityTier.GREAT, + [MoveId.PSYCHIC_NOISE]: RarityTier.GREAT, + [MoveId.UPPER_HAND]: RarityTier.COMMON, }; diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 1a1a3774f8f..3dca0c0980b 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -11,11 +11,11 @@ import { ChallengeType } from "#enums/challenge-type"; import { Challenges } from "#enums/challenges"; import { TypeColor, TypeShadow } from "#enums/color"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; -import { ModifierTier } from "#enums/modifier-tier"; import type { MoveId } from "#enums/move-id"; import type { MoveSourceType } from "#enums/move-source-type"; import { Nature } from "#enums/nature"; import { PokemonType } from "#enums/pokemon-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { TrainerVariant } from "#enums/trainer-variant"; @@ -453,13 +453,13 @@ export class SingleGenerationChallenge extends Challenge { .setBattleType(BattleType.TRAINER) .setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1) .setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true)) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, + .setCustomRewards({ + guaranteedRarityTiers: [ + RarityTier.ROGUE, + RarityTier.ROGUE, + RarityTier.ULTRA, + RarityTier.ULTRA, + RarityTier.ULTRA, ], allowLuckUpgrades: false, }); @@ -470,14 +470,14 @@ export class SingleGenerationChallenge extends Challenge { .setBattleType(BattleType.TRAINER) .setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1) .setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true)) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, + .setCustomRewards({ + guaranteedRarityTiers: [ + RarityTier.ROGUE, + RarityTier.ROGUE, + RarityTier.ULTRA, + RarityTier.ULTRA, + RarityTier.ULTRA, + RarityTier.ULTRA, ], allowLuckUpgrades: false, }); diff --git a/src/data/data-lists.ts b/src/data/data-lists.ts index ae3d0acc77f..a8db163fa69 100644 --- a/src/data/data-lists.ts +++ b/src/data/data-lists.ts @@ -1,11 +1,17 @@ import type { Ability } from "#abilities/ability"; import type { PokemonSpecies } from "#data/pokemon-species"; -import type { ModifierTypes } from "#modifiers/modifier-type"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { RewardId } from "#enums/reward-id"; +import type { TrainerItemId } from "#enums/trainer-item-id"; +import type { HeldItem } from "#items/held-item"; +import type { TrainerItem } from "#items/trainer-item"; import type { Move } from "#moves/move"; +import type { RewardFunc } from "#types/rewards"; export const allAbilities: Ability[] = []; export const allMoves: Move[] = []; export const allSpecies: PokemonSpecies[] = []; -// TODO: Figure out what this is used for and provide an appropriate tsdoc comment -export const modifierTypes = {} as ModifierTypes; +export const allHeldItems: Record = {}; +export const allTrainerItems: Record = {}; +export const allRewards: Record = {}; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index bde5f2977d8..098a2febf77 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -23,7 +23,7 @@ import { } from "#data/battler-tags"; import { getBerryEffectFunc } from "#data/berry"; import { applyChallenges } from "#data/challenge"; -import { allAbilities, allMoves } from "#data/data-lists"; +import { allAbilities, allHeldItems, allMoves } from "#data/data-lists"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers"; import { DelayedAttackTag } from "#data/positional-tags/positional-tag"; import { @@ -43,8 +43,8 @@ import { BiomeId } from "#enums/biome-id"; import { ChallengeType } from "#enums/challenge-type"; import { Command } from "#enums/command"; import { FieldPosition } from "#enums/field-position"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; import { HitResult } from "#enums/hit-result"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ChargeAnim } from "#enums/move-anims-common"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; @@ -69,14 +69,10 @@ import { SwitchType } from "#enums/switch-type"; import { WeatherType } from "#enums/weather-type"; import { MoveUsedEvent } from "#events/battle-scene"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; -import { - AttackTypeBoosterModifier, - BerryModifier, - PokemonHeldItemModifier, - PokemonMoveAccuracyBoosterModifier, - PokemonMultiHitModifier, - PreserveBerryModifier, -} from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; +import { BerryHeldItem, berryTypeToHeldItem } from "#items/berry"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { TrainerItemEffect } from "#items/trainer-item"; import { applyMoveAttrs } from "#moves/apply-attrs"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves"; import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils"; @@ -779,7 +775,7 @@ export abstract class Move implements Localizable { const isOhko = this.hasAttr("OneHitKOAccuracyAttr"); if (!isOhko) { - globalScene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); + applyHeldItems(HeldItemEffect.ACCURACY_BOOSTER, { pokemon: user, moveAccuracy: moveAccuracy }); } if (globalScene.arena.weather?.weatherType === WeatherType.FOG) { @@ -833,9 +829,15 @@ export abstract class Move implements Localizable { } // Non-priority, single-hit moves of the user's Tera Type are always a bare minimum of 60 power - 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; } @@ -865,7 +867,11 @@ export 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(HeldItemEffect.ATTACK_TYPE_BOOST, { + pokemon: source, + moveType: typeChangeHolder.value, + movePower: power, + }); } if (source.getTag(HelpingHandTag)) { @@ -928,7 +934,7 @@ export 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`) @@ -1577,7 +1583,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); @@ -2636,35 +2642,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; } } @@ -2710,10 +2714,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) { @@ -2724,29 +2728,26 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Decrease item amount and update icon target.loseHeldItem(removedItem); - globalScene.updateModifiers(target.isPlayer()); + globalScene.updateItems(target.isPlayer()); if (this.berriesOnly) { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); + globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:incineratedItem", + { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: allHeldItems[removedItem].name })); } else { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:knockedOffItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); + globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:knockedOffItem", + { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: allHeldItems[removedItem].name })); } return true; } - getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { - return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[]; - } - getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - const heldItems = this.getTargetHeldItems(target); + const heldItems = target.getHeldItems(); return heldItems.length ? 5 : 0; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - const heldItems = this.getTargetHeldItems(target); + const heldItems = target.getHeldItems(); return heldItems.length ? -5 : 0; } } @@ -2755,7 +2756,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); } @@ -2784,9 +2785,9 @@ export class EatBerryAttr extends MoveEffectAttr { this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; const preserve = new BooleanHolder(false); // check for berry pouch preservation - globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); + globalScene.applyPlayerItems(TrainerItemEffect.PRESERVE_BERRY, {pokemon: pokemon, doPreserve: preserve}); if (!preserve.value) { - this.reduceBerryModifier(pokemon); + this.reduceBerryItem(pokemon); } // Don't update harvest for berries preserved via Berry pouch (no item dupes lol) @@ -2795,16 +2796,15 @@ export class EatBerryAttr extends MoveEffectAttr { return true; } - getTargetHeldBerries(target: Pokemon): BerryModifier[] { - return globalScene.findModifiers(m => m instanceof BerryModifier - && (m as BerryModifier).pokemonId === target.id, target.isPlayer()) as BerryModifier[]; + getTargetHeldBerries(target: Pokemon): HeldItemId[] { + return target.getHeldItems().filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY)); } - reduceBerryModifier(target: Pokemon) { + reduceBerryItem(target: Pokemon) { if (this.chosenBerry) { target.loseHeldItem(this.chosenBerry); } - globalScene.updateModifiers(target.isPlayer()); + globalScene.updateItems(target.isPlayer()); } @@ -2818,10 +2818,10 @@ export class EatBerryAttr extends MoveEffectAttr { */ protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) { // consumer eats berry, owner triggers unburden and similar effects - getBerryEffectFunc(this.chosenBerry.berryType)(consumer); + getBerryEffectFunc((allHeldItems[this.chosenBerry] as BerryHeldItem).berryType)(consumer); applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner}); applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer}); - consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest); + consumer.recordEatenBerry((allHeldItems[this.chosenBerry] as BerryHeldItem).berryType, updateHarvest); } } @@ -2860,9 +2860,9 @@ export class StealEatBerryAttr extends EatBerryAttr { // pick a random berry and eat it this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; applyAbAttrs("PostItemLostAbAttr", {pokemon: target}); - const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); + const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: allHeldItems[this.chosenBerry].name }); globalScene.phaseManager.queueMessage(message); - this.reduceBerryModifier(target); + this.reduceBerryItem(target); this.eatBerry(user, target); return true; @@ -6479,9 +6479,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); @@ -8076,14 +8073,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; }; @@ -9380,7 +9377,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. @@ -10106,7 +10103,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), @@ -10855,7 +10852,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 5b2805f9310..4bbabe73ee4 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -1,12 +1,12 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; import type { IEggOptions } from "#data/egg"; import { EggSourceType } from "#enums/egg-source-types"; import { EggTier } from "#enums/egg-type"; -import { ModifierTier } from "#enums/modifier-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; @@ -150,7 +150,7 @@ export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder. }, async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; - // Battle the stat trainer for an Egg and great rewards + // Battle the stat trainer for an Egg and great allRewards const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; await transitionMysteryEncounterIntroVisuals(); @@ -164,8 +164,8 @@ export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder. encounter.setDialogueToken("eggType", i18next.t(`${namespace}:eggTypes.epic`)); setEncounterRewards( { - guaranteedModifierTypeFuncs: [modifierTypes.SACRED_ASH], - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA], + guaranteedRewardSpecs: [RewardId.SACRED_ASH], + guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.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 d6058eb9eaf..8a6d2daebb0 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -1,8 +1,8 @@ import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; +import { allHeldItems } from "#data/data-lists"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; -import type { BerryType } from "#enums/berry-type"; +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveUseMode } from "#enums/move-use-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -12,37 +12,44 @@ import { PokeballType } from "#enums/pokeball"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import { TrainerSlot } from "#enums/trainer-slot"; +import type { MysteryEncounterSpriteConfig } from "#field/mystery-encounter-intro"; import type { Pokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon"; -import { BerryModifier, PokemonInstantReviveModifier } from "#modifiers/modifier"; -import type { BerryModifierType, PokemonHeldItemModifierType } from "#modifiers/modifier-type"; +import type { HeldItemConfiguration, HeldItemSpecs, PokemonItemMap } from "#items/held-item-data-types"; +import { getPartyBerries } from "#items/item-utility"; import { PokemonMove } from "#moves/pokemon-move"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#mystery-encounters/encounter-phase-utils"; -import { - applyModifierTypeToPlayerPokemon, - catchPokemon, - getHighestLevelPlayerPokemon, -} from "#mystery-encounters/encounter-pokemon-utils"; +import { catchPokemon, getHighestLevelPlayerPokemon } from "#mystery-encounters/encounter-pokemon-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; -import { PersistentModifierRequirement } from "#mystery-encounters/mystery-encounter-requirements"; -import type { HeldModifierConfig } from "#types/held-modifier-config"; -import { randInt } from "#utils/common"; +import { HeldItemRequirement } from "#mystery-encounters/mystery-encounter-requirements"; +import { pickWeightedIndex, randInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/absoluteAvarice"; +function berrySprite(spriteKey: string, x: number, y: number): MysteryEncounterSpriteConfig { + return { + spriteKey: spriteKey, + fileRoot: "items", + isItem: true, + x: x, + y: y, + hidden: true, + disableAnimation: true, + }; +} + /** * Absolute Avarice encounter. * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805} @@ -53,7 +60,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde ) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(20, 180) - .withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 6)) // Must have at least 6 berries to spawn + .withSceneRequirement(new HeldItemRequirement(HeldItemCategoryId.BERRY, 6)) // Must have at least 6 berries to spawn .withFleeAllowed(false) .withIntroSpriteConfigs([ { @@ -74,105 +81,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde repeat: true, x: -5, }, - { - spriteKey: "lum_berry", - fileRoot: "items", - isItem: true, - x: 7, - y: -14, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "salac_berry", - fileRoot: "items", - isItem: true, - x: 2, - y: 4, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "lansat_berry", - fileRoot: "items", - isItem: true, - x: 32, - y: 5, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "liechi_berry", - fileRoot: "items", - isItem: true, - x: 6, - y: -5, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "sitrus_berry", - fileRoot: "items", - isItem: true, - x: 7, - y: 8, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "enigma_berry", - fileRoot: "items", - isItem: true, - x: 26, - y: -4, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "leppa_berry", - fileRoot: "items", - isItem: true, - x: 16, - y: -27, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "petaya_berry", - fileRoot: "items", - isItem: true, - x: 30, - y: -17, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "ganlon_berry", - fileRoot: "items", - isItem: true, - x: 16, - y: -11, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "apicot_berry", - fileRoot: "items", - isItem: true, - x: 14, - y: -2, - hidden: true, - disableAnimation: true, - }, - { - spriteKey: "starf_berry", - fileRoot: "items", - isItem: true, - x: 18, - y: 9, - hidden: true, - disableAnimation: true, - }, + berrySprite("lum_berry", 7, -14), + berrySprite("salac_berry", 2, 4), + berrySprite("lansat_berry", 32, 5), + berrySprite("liechi_berry", 6, -5), + berrySprite("sitrus_berry", 7, 8), + berrySprite("enigma_berry", 26, -4), + berrySprite("leppa_berry", 16, -27), + berrySprite("petaya_berry", 30, -17), + berrySprite("ganlon_berry", 16, -11), + berrySprite("apicot_berry", 14, -2), + berrySprite("starf_berry", 18, 9), ]) .withHideWildIntroMessage(true) .withAutoHideIntroVisuals(false) @@ -191,35 +110,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde globalScene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); globalScene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); - // Get all player berry items, remove from party, and store reference - const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + // Get all berries in party, with references to the pokemon + const berryItems = getPartyBerries(); - // Sort berries by party member ID to more easily re-add later if necessary - const berryItemsMap = new Map(); - globalScene.getPlayerParty().forEach(pokemon => { - const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); - if (pokemonBerries?.length > 0) { - berryItemsMap.set(pokemon.id, pokemonBerries); - } + encounter.misc = { berryItemsMap: berryItems }; + + // Adds stolen berries to the Greedent item configuration + const bossHeldItemConfig: HeldItemConfiguration = []; + berryItems.forEach(map => { + bossHeldItemConfig.push({ entry: map.item, count: 1 }); }); - encounter.misc = { berryItemsMap }; - - // Generates copies of the stolen berries to put on the Greedent - const bossModifierConfigs: HeldModifierConfig[] = []; - berryItems.forEach(berryMod => { - // Can't define stack count on a ModifierType, have to just create separate instances for each stack - // Overflow berries will be "lost" on the boss, but it's un-catchable anyway - for (let i = 0; i < berryMod.stackCount; i++) { - const modifierType = generateModifierType(modifierTypes.BERRY, [ - berryMod.berryType, - ]) as PokemonHeldItemModifierType; - bossModifierConfigs.push({ modifier: modifierType }); - } - }); - - // Do NOT remove the real berries yet or else it will be persisted in the session data - // +1 SpDef below wave 50, SpDef and Speed otherwise const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] = globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.SPDEF, Stat.SPD]; @@ -234,7 +135,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde bossSegments: 3, shiny: false, // Shiny lock because of consistency issues between the different options moveSet: [MoveId.THRASH, MoveId.CRUNCH, MoveId.BODY_PRESS, MoveId.SLACK_OFF], - modifierConfigs: bossModifierConfigs, + heldItemConfig: bossHeldItemConfig, tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { queueEncounterMessage(`${namespace}:option.1.boss_enraged`); @@ -261,12 +162,12 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde // Remove the berries from the party // Session has been safely saved at this point, so data won't be lost - const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; - berryItems.forEach(berryMod => { - globalScene.removeModifier(berryMod); + const berryItems = getPartyBerries(); + berryItems.forEach(map => { + globalScene.getPokemonById(map.pokemonId)?.heldItemManager.remove(map.item.id as HeldItemId); }); - globalScene.updateModifiers(true); + globalScene.updateItems(true); return true; }) @@ -286,19 +187,14 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde const encounter = globalScene.currentBattle.mysteryEncounter!; // Provides 1x Reviver Seed to each party member at end of battle - const revSeed = generateModifierType(modifierTypes.REVIVER_SEED); encounter.setDialogueToken( "foodReward", - revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"), + allHeldItems[HeldItemId.REVIVER_SEED].name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"), ); const givePartyPokemonReviverSeeds = () => { const party = globalScene.getPlayerParty(); party.forEach(p => { - const heldItems = p.getHeldItems(); - if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) { - const seedModifier = revSeed.newModifier(p); - globalScene.addModifier(seedModifier, false, false, false, true); - } + p.heldItemManager.add(HeldItemId.REVIVER_SEED); }); queueEncounterMessage(`${namespace}:option.1.food_stash`); }; @@ -329,28 +225,27 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde }) .withOptionPhase(async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; - const berryMap = encounter.misc.berryItemsMap; + const berryMap = encounter.misc.berryItemsMap as PokemonItemMap[]; // 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); + const stolenBerries = berryMap.filter(map => map.pokemonId === pokemon.id); + const stolenBerryCount = stolenBerries.reduce((a, b) => a + (b.item as HeldItemSpecs).stack, 0); + const returnedBerryCount = Math.floor(((stolenBerryCount ?? 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); + const berryWeights = stolenBerries.map(b => (b.item as HeldItemSpecs).stack); + const which = pickWeightedIndex(berryWeights) ?? 0; + const randBerry = stolenBerries[which]; + pokemon.heldItemManager.add(randBerry.item.id as HeldItemId); + (randBerry.item as HeldItemSpecs).stack -= 1; } } }); - await globalScene.updateModifiers(true); + await globalScene.updateItems(true); await transitionMysteryEncounterIntroVisuals(true, true, 500); leaveEncounterWithoutBattle(true); diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index 619acabe200..0a9a776472e 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -1,13 +1,13 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { speciesStarterCosts } from "#balance/starters"; -import { modifierTypes } from "#data/data-lists"; +import { allRewards, allTrainerItems } from "#data/data-lists"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { - generateModifierType, leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, @@ -109,8 +109,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB } } - const shinyCharm = generateModifierType(modifierTypes.SHINY_CHARM); - encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name")); + const name = allTrainerItems[TrainerItemId.SHINY_CHARM].name; + encounter.setDialogueToken("itemName", name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name")); encounter.setDialogueToken("liepardName", getPokemonSpecies(SpeciesId.LIEPARD).getName()); return true; @@ -136,7 +136,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB }) .withOptionPhase(async () => { // Give the player a Shiny Charm - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.SHINY_CHARM); + globalScene.phaseManager.unshiftNew("RewardPhase", allRewards.SHINY_CHARM); leaveEncounterWithoutBattle(true); }) .build(), @@ -184,7 +184,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index a827c3fcc0a..2abfc81b1ea 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -1,23 +1,22 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import { modifierTypes } from "#data/data-lists"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RewardId } from "#enums/reward-id"; +import { RewardPoolType } from "#enums/reward-pool-type"; import { PERMANENT_STATS, Stat } from "#enums/stat"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; -import { BerryModifier } from "#modifiers/modifier"; -import type { BerryModifierType, ModifierTypeOption } from "#modifiers/modifier-type"; -import { regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; +import { berryTypeToHeldItem } from "#items/berry"; +import type { RewardOption } from "#items/reward"; +import { generateRewardPoolWeights, getRewardPoolForType } from "#items/reward-pool-utils"; +import { generateRewardOptionFromId } from "#items/reward-utils"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, - generateModifierTypeOption, getRandomEncounterSpecies, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, @@ -25,7 +24,6 @@ import { setEncounterRewards, } from "#mystery-encounters/encounter-phase-utils"; import { - applyModifierTypeToPlayerPokemon, getEncounterPokemonLevelForWave, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon, @@ -90,7 +88,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. : globalScene.currentBattle.waveIndex > 40 ? 4 : 2; - regenerateModifierPoolThresholds(globalScene.getPlayerParty(), ModifierPoolType.PLAYER, 0); + generateRewardPoolWeights(getRewardPoolForType(RewardPoolType.PLAYER), globalScene.getPlayerParty(), 0); encounter.misc = { numBerries }; const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); @@ -161,20 +159,16 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. } }; - const shopOptions: ModifierTypeOption[] = []; + const shopOptions: RewardOption[] = []; for (let i = 0; i < 5; i++) { // Generate shop berries - const mod = generateModifierTypeOption(modifierTypes.BERRY); + const mod = generateRewardOptionFromId(RewardId.BERRY); if (mod) { shopOptions.push(mod); } } - setEncounterRewards( - { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, - undefined, - doBerryRewards, - ); + setEncounterRewards({ guaranteedRewardOptions: shopOptions, fillRemaining: false }, undefined, doBerryRewards); await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); }, ) @@ -192,10 +186,10 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. const speedDiff = fastestPokemon.getStat(Stat.SPD) / (enemySpeed * 1.1); const numBerries: number = encounter.misc.numBerries; - const shopOptions: ModifierTypeOption[] = []; + const shopOptions: RewardOption[] = []; for (let i = 0; i < 5; i++) { // Generate shop berries - const mod = generateModifierTypeOption(modifierTypes.BERRY); + const mod = generateRewardOptionFromId(RewardId.BERRY); if (mod) { shopOptions.push(mod); } @@ -248,7 +242,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. }; setEncounterRewards( { - guaranteedModifierTypeOptions: shopOptions, + guaranteedRewardOptions: shopOptions, fillRemaining: false, }, undefined, @@ -281,7 +275,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. setEncounterExp(fastestPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs![0].species.baseExp); setEncounterRewards( { - guaranteedModifierTypeOptions: shopOptions, + guaranteedRewardOptions: shopOptions, fillRemaining: false, }, undefined, @@ -303,7 +297,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, @@ -312,35 +306,17 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. function tryGiveBerry(prioritizedPokemon?: PlayerPokemon) { const berryType = randSeedItem(getEnumValues(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 d60ebe690ac..8b9edd9f51e 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -1,32 +1,26 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { allMoves, modifierTypes } from "#data/data-lists"; -import { ModifierTier } from "#enums/modifier-tier"; +import { allHeldItems, allMoves } from "#data/data-lists"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { PokemonType } from "#enums/pokemon-type"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerType } from "#enums/trainer-type"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; -import type { PokemonHeldItemModifier } from "#modifiers/modifier"; -import { - AttackTypeBoosterModifier, - BypassSpeedChanceModifier, - ContactHeldItemTransferChanceModifier, - GigantamaxAccessModifier, - MegaEvolutionAccessModifier, -} from "#modifiers/modifier"; -import type { AttackTypeBoosterModifierType, ModifierTypeOption } from "#modifiers/modifier-type"; +import type { RewardOption } from "#items/reward"; +import { generateRewardOptionFromId } from "#items/reward-utils"; import { PokemonMove } from "#moves/pokemon-move"; import { getEncounterText, showEncounterDialogue } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, - generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectOptionThenPokemon, @@ -39,9 +33,8 @@ import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { - AttackTypeBoosterHeldItemTypeRequirement, CombinationPokemonRequirement, - HeldItemRequirement, + HoldingItemRequirement, TypeRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; import { getRandomPartyMemberFunc, trainerConfigs } from "#trainers/trainer-config"; @@ -140,6 +133,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, @@ -183,8 +178,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 HoldingItemRequirement(REQUIRED_ITEMS, 1), new TypeRequirement(PokemonType.BUG, false, 1), ), ) @@ -256,13 +250,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; @@ -298,7 +286,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde moveTutorOptions, }; - // Assigns callback that teaches move before continuing to rewards + // Assigns callback that teaches move before continuing to RewardId encounter.onRewards = doBugTypeMoveTutor; setEncounterRewards({ fillRemaining: true }); @@ -318,7 +306,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde // Player shows off their bug types const encounter = globalScene.currentBattle.mysteryEncounter!; - // Player gets different rewards depending on the number of bug types they have + // Player gets different RewardId depending on the number of bug types they have const numBugTypes = globalScene.getPlayerParty().filter(p => p.isOfType(PokemonType.BUG, true)).length; const numBugTypesText = i18next.t(`${namespace}:numBugTypes`, { count: numBugTypes, @@ -327,7 +315,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde if (numBugTypes < 2) { setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.SUPER_LURE, modifierTypes.GREAT_BALL], + guaranteedRewardSpecs: [RewardId.SUPER_LURE, RewardId.GREAT_BALL], fillRemaining: false, }); encounter.selectedOption!.dialogue!.selected = [ @@ -338,7 +326,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde ]; } else if (numBugTypes < 4) { setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.QUICK_CLAW, modifierTypes.MAX_LURE, modifierTypes.ULTRA_BALL], + guaranteedRewardSpecs: [HeldItemId.QUICK_CLAW, RewardId.MAX_LURE, RewardId.ULTRA_BALL], fillRemaining: false, }); encounter.selectedOption!.dialogue!.selected = [ @@ -349,7 +337,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde ]; } else if (numBugTypes < 6) { setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.GRIP_CLAW, modifierTypes.MAX_LURE, modifierTypes.ROGUE_BALL], + guaranteedRewardSpecs: [HeldItemId.GRIP_CLAW, RewardId.MAX_LURE, RewardId.ROGUE_BALL], fillRemaining: false, }); encounter.selectedOption!.dialogue!.selected = [ @@ -361,38 +349,38 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde } else { // If the player has any evolution/form change items that are valid for their party, // spawn one of those items in addition to Dynamax Band, Mega Band, and Master Ball - const modifierOptions: ModifierTypeOption[] = [generateModifierTypeOption(modifierTypes.MASTER_BALL)!]; - const specialOptions: ModifierTypeOption[] = []; + const rewardOptions: RewardOption[] = [generateRewardOptionFromId(RewardId.MASTER_BALL)!]; + const specialOptions: RewardOption[] = []; - if (!globalScene.findModifier(m => m instanceof MegaEvolutionAccessModifier)) { - modifierOptions.push(generateModifierTypeOption(modifierTypes.MEGA_BRACELET)!); + if (!globalScene.trainerItems.hasItem(TrainerItemId.MEGA_BRACELET)) { + rewardOptions.push(generateRewardOptionFromId(TrainerItemId.MEGA_BRACELET)!); } - if (!globalScene.findModifier(m => m instanceof GigantamaxAccessModifier)) { - modifierOptions.push(generateModifierTypeOption(modifierTypes.DYNAMAX_BAND)!); + if (!globalScene.trainerItems.hasItem(TrainerItemId.DYNAMAX_BAND)) { + rewardOptions.push(generateRewardOptionFromId(TrainerItemId.DYNAMAX_BAND)!); } - const nonRareEvolutionModifier = generateModifierTypeOption(modifierTypes.EVOLUTION_ITEM); - if (nonRareEvolutionModifier) { - specialOptions.push(nonRareEvolutionModifier); + const nonRareEvolutionReward = generateRewardOptionFromId(RewardId.EVOLUTION_ITEM); + if (nonRareEvolutionReward) { + specialOptions.push(nonRareEvolutionReward); } - const rareEvolutionModifier = generateModifierTypeOption(modifierTypes.RARE_EVOLUTION_ITEM); - if (rareEvolutionModifier) { - specialOptions.push(rareEvolutionModifier); + const rareEvolutionReward = generateRewardOptionFromId(RewardId.RARE_EVOLUTION_ITEM); + if (rareEvolutionReward) { + specialOptions.push(rareEvolutionReward); } - const formChangeModifier = generateModifierTypeOption(modifierTypes.FORM_CHANGE_ITEM); - if (formChangeModifier) { - specialOptions.push(formChangeModifier); + const formChangeReward = generateRewardOptionFromId(RewardId.FORM_CHANGE_ITEM); + if (formChangeReward) { + specialOptions.push(formChangeReward); } - const rareFormChangeModifier = generateModifierTypeOption(modifierTypes.RARE_FORM_CHANGE_ITEM); - if (rareFormChangeModifier) { - specialOptions.push(rareFormChangeModifier); + const rareFormChangeReward = generateRewardOptionFromId(RewardId.RARE_FORM_CHANGE_ITEM); + if (rareFormChangeReward) { + specialOptions.push(rareFormChangeReward); } if (specialOptions.length > 0) { // TODO: should this use `randSeedItem`? - modifierOptions.push(specialOptions[randSeedInt(specialOptions.length)]); + rewardOptions.push(specialOptions[randSeedInt(specialOptions.length)]); } setEncounterRewards({ - guaranteedModifierTypeOptions: modifierOptions, + guaranteedRewardOptions: rewardOptions, fillRemaining: false, }); encounter.selectedOption!.dialogue!.selected = [ @@ -414,8 +402,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 HoldingItemRequirement(REQUIRED_ITEMS, 1), ), ) .withDialogue({ @@ -438,25 +425,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 => REQUIRED_ITEMS.some(i => i === item)); - 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; }, @@ -467,14 +448,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) - ); - }); + const hasValidItem = pokemon.getHeldItems().some(item => REQUIRED_ITEMS.some(i => i === item)); if (!hasValidItem) { return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; } @@ -486,18 +460,18 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde }) .withOptionPhase(async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; - const modifier = encounter.misc.chosenModifier; + const lostItem = encounter.misc.chosenItem; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; - chosenPokemon.loseHeldItem(modifier, false); - globalScene.updateModifiers(true, true); + chosenPokemon.loseHeldItem(lostItem, false); + globalScene.updateItems(true); - const bugNet = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; - bugNet.type.tier = ModifierTier.ROGUE; + const bugNet = generateRewardOptionFromId(TrainerItemId.GOLDEN_BUG_NET)!; + bugNet.type.tier = RarityTier.ROGUE; setEncounterRewards({ - guaranteedModifierTypeOptions: [bugNet], - guaranteedModifierTypeFuncs: [modifierTypes.REVIVER_SEED], + guaranteedRewardOptions: [bugNet], + guaranteedRewardSpecs: [HeldItemId.REVIVER_SEED], fillRemaining: false, }); leaveEncounterWithoutBattle(true); @@ -771,7 +745,7 @@ function doBugTypeMoveTutor(): Promise { ); } - // Complete battle and go to rewards + // Complete battle and go to RewardId resolve(); }); } diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 1c85cb7595c..de9e9b0f3b1 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -1,15 +1,13 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { EncounterBattleAnim } from "#data/battle-anims"; -import { allAbilities, modifierTypes } from "#data/data-lists"; +import { allAbilities } from "#data/data-lists"; import { CustomPokemonData } from "#data/pokemon-data"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; -import { BerryType } from "#enums/berry-type"; import { Challenges } from "#enums/challenges"; import { EncounterAnim } from "#enums/encounter-anims"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { ModifierTier } from "#enums/modifier-tier"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; import { MoveCategory } from "#enums/move-category"; import { MoveId } from "#enums/move-id"; import { MoveUseMode } from "#enums/move-use-mode"; @@ -18,17 +16,17 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { PokemonType } from "#enums/pokemon-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; -import { BerryModifier } from "#modifiers/modifier"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; +import { getHeldItemTier } from "#items/held-item-default-tiers"; +import { assignItemsFromConfiguration } from "#items/held-item-pool"; import { PokemonMove } from "#moves/pokemon-move"; import { showEncounterDialogue, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, @@ -36,10 +34,7 @@ import { setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#mystery-encounters/encounter-phase-utils"; -import { - applyAbilityOverrideToPokemon, - applyModifierTypeToPlayerPokemon, -} from "#mystery-encounters/encounter-pokemon-utils"; +import { applyAbilityOverrideToPokemon } from "#mystery-encounters/encounter-pokemon-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; @@ -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) ?? RarityTier.ULTRA; + const stack = mostHeldItemsPokemon.heldItemManager.getStack(m); + if (tier === RarityTier.ROGUE) { + numRogue += stack; + } else if (tier === RarityTier.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/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 94006a43837..2db0580fccc 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -1,11 +1,11 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { EncounterBattleAnim } from "#data/battle-anims"; -import { modifierTypes } from "#data/data-lists"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; import { EncounterAnim } from "#enums/encounter-anims"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveUseMode } from "#enums/move-use-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -153,7 +153,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder } const oricorioData = new PokemonData(enemyPokemon); - const oricorio = globalScene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, false, oricorioData); + const oricorio = globalScene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, false, [], oricorioData); // Adds a real Pokemon sprite to the field (required for the animation) for (const enemyPokemon of globalScene.getEnemyParty()) { @@ -219,7 +219,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder await hideOricorioPokemon(); setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.BATON], + guaranteedRewardSpecs: [HeldItemId.BATON], fillRemaining: true, }); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 29517ac2531..aede0c92168 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -1,14 +1,13 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; +import { allRewards } from "#data/data-lists"; import { Challenges } from "#enums/challenges"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; -import type { PokemonHeldItemModifier } from "#modifiers/modifier"; -import { PokemonFormChangeItemModifier } from "#modifiers/modifier"; +import type { HeldItemConfiguration } from "#items/held-item-data-types"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils"; import { initBattleWithEnemyConfig, leaveEncounterWithoutBattle } from "#mystery-encounters/encounter-phase-utils"; import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#mystery-encounters/encounter-pokemon-utils"; @@ -146,7 +145,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!; @@ -155,13 +154,13 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE // Store removed pokemon types encounter.misc = { removedTypes: removedPokemon.getTypes(), - modifiers, + itemConfig: itemConfig, }; }) .withOptionPhase(async () => { // Give the player 5 Rogue Balls const encounter = globalScene.currentBattle.mysteryEncounter!; - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.ROGUE_BALL); + globalScene.phaseManager.unshiftNew("RewardPhase", allRewards.ROGUE_BALL); // Start encounter with random legendary (7-10 starter strength) that has level additive // If this is a mono-type challenge, always ensure the required type is filtered for @@ -173,7 +172,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]; @@ -181,12 +180,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; @@ -210,7 +204,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 66f50f134dd..d0cd73f1bc6 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -1,35 +1,26 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { timedEventManager } from "#app/global-event-manager"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; +import { allHeldItems, allRewards } from "#data/data-lists"; +import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; -import type { PokemonHeldItemModifier, PokemonInstantReviveModifier } from "#modifiers/modifier"; -import { - BerryModifier, - HealingBoosterModifier, - LevelIncrementBoosterModifier, - MoneyMultiplierModifier, - PreserveBerryModifier, -} from "#modifiers/modifier"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { - generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#mystery-encounters/encounter-phase-utils"; -import { applyModifierTypeToPlayerPokemon } from "#mystery-encounters/encounter-pokemon-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { CombinationPokemonRequirement, - HeldItemRequirement, + HoldingItemRequirement, MoneyRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; import i18next from "#plugins/i18n"; @@ -41,32 +32,39 @@ import { getPokemonSpecies } from "#utils/pokemon-utils"; const namespace = "mysteryEncounters/delibirdy"; /** Berries only */ -const OPTION_2_ALLOWED_MODIFIERS = ["BerryModifier", "PokemonInstantReviveModifier"]; +const OPTION_2_ALLOWED_HELD_ITEMS = [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", -]; +/** Disallowed items are berries, Reviver Seeds, and Vitamins */ +const OPTION_3_DISALLOWED_HELD_ITEMS = [HeldItemCategoryId.BERRY, HeldItemId.REVIVER_SEED]; const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2; +async function backupOption() { + globalScene.getPlayerPokemon()?.heldItemManager.add(HeldItemId.SHELL_BELL); + globalScene.playSound("item_fanfare"); + await showEncounterText( + i18next.t("battle:rewardGain", { + modifierName: allHeldItems[HeldItemId.SHELL_BELL].name, + }), + null, + undefined, + true, + ); + doEventReward(); +} + const doEventReward = () => { const event_buff = timedEventManager.getDelibirdyBuff(); if (event_buff.length > 0) { const candidates = event_buff.filter(c => { - const mtype = generateModifierType(modifierTypes[c]); - const existingCharm = globalScene.findModifier(m => m.type.id === mtype?.id); - return !(existingCharm && existingCharm.getStackCount() >= existingCharm.getMaxStackCount()); + const fullStack = globalScene.trainerItems.isMaxStack(c); + return !fullStack; }); if (candidates.length > 0) { - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes[randSeedItem(candidates)]); + globalScene.phaseManager.unshiftNew("RewardPhase", allRewards[randSeedItem(candidates)]); } else { // At max stacks, give a Voucher instead - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.VOUCHER); + globalScene.phaseManager.unshiftNew("RewardPhase", allRewards.VOUCHER); } } }; @@ -85,8 +83,8 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withPrimaryPokemonRequirement( CombinationPokemonRequirement.Some( // Must also have either option 2 or 3 available to spawn - new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), - new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true), + new HoldingItemRequirement(OPTION_2_ALLOWED_HELD_ITEMS), + new HoldingItemRequirement(OPTION_3_DISALLOWED_HELD_ITEMS, 1, true), ), ) .withIntroSpriteConfigs([ @@ -164,22 +162,13 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withOptionPhase(async () => { // Give the player an Amulet Coin // Check if the player has max stacks of that item already - const existing = globalScene.findModifier(m => m instanceof MoneyMultiplierModifier) as MoneyMultiplierModifier; + const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.AMULET_COIN); - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { + if (fullStack) { // At max stacks, give the first party pokemon a Shell Bell instead - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell); - globalScene.playSound("item_fanfare"); - await showEncounterText( - i18next.t("battle:rewardGain", { modifierName: shellBell.name }), - null, - undefined, - true, - ); - doEventReward(); + backupOption(); } else { - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.AMULET_COIN); + globalScene.phaseManager.unshiftNew("RewardPhase", allRewards.AMULET_COIN); doEventReward(); } @@ -189,7 +178,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with ) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS)) + .withPrimaryPokemonRequirement(new HoldingItemRequirement(OPTION_2_ALLOWED_HELD_ITEMS)) .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, @@ -204,19 +193,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_HELD_ITEMS, 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; }, @@ -239,59 +226,35 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with }) .withOptionPhase(async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; - const modifier: BerryModifier | PokemonInstantReviveModifier = encounter.misc.chosenModifier; + const chosenItem: HeldItemId = encounter.misc.chosenItem; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed - if (modifier instanceof BerryModifier) { + if (isItemInCategory(chosenItem, HeldItemCategoryId.BERRY)) { // Check if the player has max stacks of that Candy Jar already - const existing = globalScene.findModifier( - m => m instanceof LevelIncrementBoosterModifier, - ) as LevelIncrementBoosterModifier; + const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.CANDY_JAR); - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { + if (fullStack) { // At max stacks, give the first party pokemon a Shell Bell instead - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell); - globalScene.playSound("item_fanfare"); - await showEncounterText( - i18next.t("battle:rewardGain", { - modifierName: shellBell.name, - }), - null, - undefined, - true, - ); - doEventReward(); + backupOption(); } else { - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.CANDY_JAR); + globalScene.phaseManager.unshiftNew("RewardPhase", allRewards.CANDY_JAR); doEventReward(); } } else { // Check if the player has max stacks of that Berry Pouch already - const existing = globalScene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; + const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.BERRY_POUCH); - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { + if (fullStack) { // At max stacks, give the first party pokemon a Shell Bell instead - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell); - globalScene.playSound("item_fanfare"); - await showEncounterText( - i18next.t("battle:rewardGain", { - modifierName: shellBell.name, - }), - null, - undefined, - true, - ); - doEventReward(); + backupOption(); } else { - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.BERRY_POUCH); + globalScene.phaseManager.unshiftNew("RewardPhase", allRewards.BERRY_POUCH); doEventReward(); } } - chosenPokemon.loseHeldItem(modifier, false); + chosenPokemon.loseHeldItem(chosenItem, false); leaveEncounterWithoutBattle(true); }) @@ -299,7 +262,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with ) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)) + .withPrimaryPokemonRequirement(new HoldingItemRequirement(OPTION_3_DISALLOWED_HELD_ITEMS, 1, true)) .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, @@ -314,21 +277,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_HELD_ITEMS, 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; }, @@ -351,30 +310,22 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with }) .withOptionPhase(async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; - const modifier = encounter.misc.chosenModifier; + const chosenItem = encounter.misc.chosenItem; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; // Check if the player has max stacks of Healing Charm already - const existing = globalScene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { + const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.HEALING_CHARM); + + if (fullStack) { // At max stacks, give the first party pokemon a Shell Bell instead - const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(globalScene.getPlayerParty()[0], shellBell); - globalScene.playSound("item_fanfare"); - await showEncounterText( - i18next.t("battle:rewardGain", { modifierName: shellBell.name }), - null, - undefined, - true, - ); - doEventReward(); + backupOption(); } else { - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.HEALING_CHARM); + globalScene.phaseManager.unshiftNew("RewardPhase", allRewards.HEALING_CHARM); doEventReward(); } - chosenPokemon.loseHeldItem(modifier, false); + chosenPokemon.loseHeldItem(chosenItem, false); leaveEncounterWithoutBattle(true); }) diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts index a45c5301a3e..bb645c5d004 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -1,12 +1,12 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; -import { modifierTypes } from "#data/data-lists"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RewardId } from "#enums/reward-id"; import { SpeciesId } from "#enums/species-id"; import { leaveEncounterWithoutBattle, setEncounterRewards } from "#mystery-encounters/encounter-phase-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; -import type { ModifierTypeFunc } from "#types/modifier-types"; +import type { RewardSpecs } from "#types/rewards"; import { randSeedInt } from "#utils/common"; /** i18n namespace for encounter */ @@ -59,23 +59,23 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu }, async () => { // Choose TMs - const modifiers: ModifierTypeFunc[] = []; + const rewards: RewardSpecs[] = []; let i = 0; while (i < 5) { // 2/2/1 weight on TM rarity const roll = randSeedInt(5); if (roll < 2) { - modifiers.push(modifierTypes.TM_COMMON); + rewards.push(RewardId.TM_COMMON); } else if (roll < 4) { - modifiers.push(modifierTypes.TM_GREAT); + rewards.push(RewardId.TM_GREAT); } else { - modifiers.push(modifierTypes.TM_ULTRA); + rewards.push(RewardId.TM_ULTRA); } i++; } setEncounterRewards({ - guaranteedModifierTypeFuncs: modifiers, + guaranteedRewardSpecs: rewards, fillRemaining: false, }); leaveEncounterWithoutBattle(); @@ -88,21 +88,21 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu }, async () => { // Choose Vitamins - const modifiers: ModifierTypeFunc[] = []; + const rewards: RewardSpecs[] = []; let i = 0; while (i < 3) { // 2/1 weight on base stat booster vs PP Up const roll = randSeedInt(3); if (roll === 0) { - modifiers.push(modifierTypes.PP_UP); + rewards.push(RewardId.PP_UP); } else { - modifiers.push(modifierTypes.BASE_STAT_BOOSTER); + rewards.push(RewardId.BASE_STAT_BOOSTER); } i++; } setEncounterRewards({ - guaranteedModifierTypeFuncs: modifiers, + guaranteedRewardSpecs: rewards, fillRemaining: false, }); leaveEncounterWithoutBattle(); @@ -115,21 +115,21 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu }, async () => { // Choose X Items - const modifiers: ModifierTypeFunc[] = []; + const rewards: RewardSpecs[] = []; let i = 0; while (i < 5) { // 4/1 weight on base stat booster vs Dire Hit const roll = randSeedInt(5); if (roll === 0) { - modifiers.push(modifierTypes.DIRE_HIT); + rewards.push(RewardId.DIRE_HIT); } else { - modifiers.push(modifierTypes.TEMP_STAT_STAGE_BOOSTER); + rewards.push(RewardId.TEMP_STAT_STAGE_BOOSTER); } i++; } setEncounterRewards({ - guaranteedModifierTypeFuncs: modifiers, + guaranteedRewardSpecs: rewards, fillRemaining: false, }); leaveEncounterWithoutBattle(); @@ -142,25 +142,25 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu }, async () => { // Choose Pokeballs - const modifiers: ModifierTypeFunc[] = []; + const rewards: RewardSpecs[] = []; let i = 0; while (i < 4) { // 10/30/20/5 weight on pokeballs const roll = randSeedInt(65); if (roll < 10) { - modifiers.push(modifierTypes.POKEBALL); + rewards.push(RewardId.POKEBALL); } else if (roll < 40) { - modifiers.push(modifierTypes.GREAT_BALL); + rewards.push(RewardId.GREAT_BALL); } else if (roll < 60) { - modifiers.push(modifierTypes.ULTRA_BALL); + rewards.push(RewardId.ULTRA_BALL); } else { - modifiers.push(modifierTypes.ROGUE_BALL); + rewards.push(RewardId.ROGUE_BALL); } i++; } setEncounterRewards({ - guaranteedModifierTypeFuncs: modifiers, + guaranteedRewardSpecs: rewards, fillRemaining: false, }); leaveEncounterWithoutBattle(); diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index 9c655e70b8c..34b13527cfd 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -1,6 +1,5 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; import { MoveCategory } from "#enums/move-category"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -9,7 +8,6 @@ import { Stat } from "#enums/stat"; import type { PlayerPokemon } from "#field/pokemon"; import type { PokemonMove } from "#moves/pokemon-move"; import { - generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, @@ -96,15 +94,15 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with const encounter = globalScene.currentBattle.mysteryEncounter!; if (encounter.misc.correctMove) { const modifiers = [ - generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.ATK])!, - generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.DEF])!, - generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, - generateModifierTypeOption(modifierTypes.DIRE_HIT)!, - generateModifierTypeOption(modifierTypes.RARER_CANDY)!, + generateRewardOptionFromId(RewardId.TEMP_STAT_STAGE_BOOSTER, [Stat.ATK])!, + generateRewardOptionFromId(RewardId.TEMP_STAT_STAGE_BOOSTER, [Stat.DEF])!, + generateRewardOptionFromId(RewardId.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateRewardOptionFromId(RewardId.DIRE_HIT)!, + generateRewardOptionFromId(RewardId.RARER_CANDY)!, ]; setEncounterRewards({ - guaranteedModifierTypeOptions: modifiers, + guaranteedRewardOptions: modifiers, fillRemaining: false, }); } @@ -144,15 +142,15 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with const encounter = globalScene.currentBattle.mysteryEncounter!; if (encounter.misc.correctMove) { const modifiers = [ - generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPATK])!, - generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPDEF])!, - generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, - generateModifierTypeOption(modifierTypes.DIRE_HIT)!, - generateModifierTypeOption(modifierTypes.RARER_CANDY)!, + generateRewardOptionFromId(RewardId.TEMP_STAT_STAGE_BOOSTER, [Stat.SPATK])!, + generateRewardOptionFromId(RewardId.TEMP_STAT_STAGE_BOOSTER, [Stat.SPDEF])!, + generateRewardOptionFromId(RewardId.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateRewardOptionFromId(RewardId.DIRE_HIT)!, + generateRewardOptionFromId(RewardId.RARER_CANDY)!, ]; setEncounterRewards({ - guaranteedModifierTypeOptions: modifiers, + guaranteedRewardOptions: modifiers, fillRemaining: false, }); } @@ -192,15 +190,15 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with const encounter = globalScene.currentBattle.mysteryEncounter!; if (encounter.misc.correctMove) { const modifiers = [ - generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.ACC])!, - generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, - generateModifierTypeOption(modifierTypes.GREAT_BALL)!, - generateModifierTypeOption(modifierTypes.IV_SCANNER)!, - generateModifierTypeOption(modifierTypes.RARER_CANDY)!, + generateRewardOptionFromId(RewardId.TEMP_STAT_STAGE_BOOSTER, [Stat.ACC])!, + generateRewardOptionFromId(RewardId.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateRewardOptionFromId(RewardId.GREAT_BALL)!, + generateRewardOptionFromId(RewardId.IV_SCANNER)!, + generateRewardOptionFromId(RewardId.RARER_CANDY)!, ]; setEncounterRewards({ - guaranteedModifierTypeOptions: modifiers, + guaranteedRewardOptions: modifiers, fillRemaining: false, }); } diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 7d2583a00cb..a3d1da01351 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -1,12 +1,13 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { EncounterBattleAnim } from "#data/battle-anims"; -import { allAbilities, modifierTypes } from "#data/data-lists"; +import { allAbilities, allHeldItems } from "#data/data-lists"; import { Gender } from "#data/gender"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { EncounterAnim } from "#enums/encounter-anims"; +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveUseMode } from "#enums/move-use-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -18,12 +19,11 @@ import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; -import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type"; +import { getNewHeldItemFromCategory } from "#items/held-item-pool"; import { PokemonMove } from "#moves/pokemon-move"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, @@ -31,11 +31,7 @@ import { setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#mystery-encounters/encounter-phase-utils"; -import { - applyAbilityOverrideToPokemon, - applyDamageToPokemon, - applyModifierTypeToPlayerPokemon, -} from "#mystery-encounters/encounter-pokemon-utils"; +import { applyAbilityOverrideToPokemon, applyDamageToPokemon } from "#mystery-encounters/encounter-pokemon-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; @@ -254,7 +250,7 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w } } - // No rewards + // No allRewards leaveEncounterWithoutBattle(true); }, ) @@ -299,19 +295,17 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w function giveLeadPokemonAttackTypeBoostItem() { // Give first party pokemon attack type boost item for free at end of battle - const leadPokemon = globalScene.getPlayerParty()?.[0]; + 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 6ee2ebcdf67..adc614fb9d6 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -1,14 +1,16 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { ModifierTier } from "#enums/modifier-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RewardPoolType } from "#enums/reward-pool-type"; +import { RarityTier } from "#enums/reward-tier"; +import { TrainerItemId } from "#enums/trainer-item-id"; import type { Pokemon } from "#field/pokemon"; -import type { ModifierTypeOption } from "#modifiers/modifier-type"; -import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; +import type { RewardOption, TrainerItemReward } from "#items/reward"; +import { generatePlayerRewardOptions, generateRewardPoolWeights, getRewardPoolForType } from "#items/reward-pool-utils"; +import { isTmReward } from "#items/reward-utils"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { @@ -89,18 +91,18 @@ 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 + ? RarityTier.MASTER : globalScene.currentBattle.waveIndex > 120 - ? ModifierTier.ROGUE + ? RarityTier.ROGUE : globalScene.currentBattle.waveIndex > 40 - ? ModifierTier.ULTRA - : ModifierTier.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 - while (!item || item.type.id.includes("TM_") || item.type.id === "CANDY_JAR") { - item = getPlayerModifierTypeOptions(1, globalScene.getPlayerParty(), [], { - guaranteedModifierTiers: [tier], + ? RarityTier.ULTRA + : RarityTier.GREAT; + generateRewardPoolWeights(getRewardPoolForType(RewardPoolType.PLAYER), globalScene.getPlayerParty(), 0); + let item: RewardOption | null = null; + // TMs and Candy Jar excluded from possible allRewards as they're too swingy in value for a singular item reward + while (!item || isTmReward(item.type) || (item.type as TrainerItemReward).itemId === TrainerItemId.CANDY_JAR) { + item = generatePlayerRewardOptions(1, globalScene.getPlayerParty(), [], { + guaranteedRarityTiers: [tier], allowLuckUpgrades: false, })[0]; } @@ -151,9 +153,9 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder. async () => { // Pick battle // Pokemon will randomly boost 1 stat by 2 stages - const item = globalScene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; + const item = globalScene.currentBattle.mysteryEncounter!.misc as RewardOption; setEncounterRewards({ - guaranteedModifierTypeOptions: [item], + guaranteedRewardOptions: [item], fillRemaining: false, }); await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); @@ -175,9 +177,9 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder. .withOptionPhase(async () => { // Pick steal const encounter = globalScene.currentBattle.mysteryEncounter!; - const item = globalScene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; + const item = globalScene.currentBattle.mysteryEncounter!.misc as RewardOption; setEncounterRewards({ - guaranteedModifierTypeOptions: [item], + guaranteedRewardOptions: [item], fillRemaining: false, }); @@ -199,7 +201,7 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder. ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index bf376c04843..51bfeea397c 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -1,10 +1,10 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import { modifierTypes } from "#data/data-lists"; import { SpeciesFormChangeActiveTrigger } from "#data/form-change-triggers"; import { getPokeballAtlasKey, getPokeballTintColor } from "#data/pokeball"; import { FieldPosition } from "#enums/field-position"; +import { HeldItemId } from "#enums/held-item-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -160,7 +160,7 @@ export const FunAndGamesEncounter: MysteryEncounter = MysteryEncounterBuilder.wi ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no RewardId or exp await transitionMysteryEncounterIntroVisuals(true, true); leaveEncounterWithoutBattle(true); return true; @@ -281,21 +281,21 @@ function handleNextTurn() { if (healthRatio < 0.03) { // Grand prize setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.MULTI_LENS], + guaranteedRewardSpecs: [HeldItemId.MULTI_LENS], fillRemaining: false, }); resultMessageKey = `${namespace}:best_result`; } else if (healthRatio < 0.15) { // 2nd prize setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.SCOPE_LENS], + guaranteedRewardSpecs: [HeldItemId.SCOPE_LENS], fillRemaining: false, }); resultMessageKey = `${namespace}:great_result`; } else if (healthRatio < 0.33) { // 3rd prize setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.WIDE_LENS], + guaranteedRewardSpecs: [HeldItemId.WIDE_LENS], fillRemaining: false, }); resultMessageKey = `${namespace}:good_result`; @@ -387,7 +387,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise { globalScene.add.existing(pokemon); globalScene.field.add(pokemon); addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.pokeball); - globalScene.updateModifiers(true); + globalScene.updateItems(true); globalScene.updateFieldScale(); pokemon.showInfo(); pokemon.playAnim(); diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 347092fe0b4..bb9c32c8d73 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -1,33 +1,30 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { timedEventManager } from "#app/global-event-manager"; import { globalScene } from "#app/global-scene"; -import { allSpecies } from "#data/data-lists"; +import { allHeldItems, allSpecies } from "#data/data-lists"; import { Gender, getGenderSymbol } from "#data/gender"; import { getNatureName } from "#data/nature"; import { getPokeballAtlasKey, getPokeballTintColor } from "#data/pokeball"; import type { PokemonSpecies } from "#data/pokemon-species"; import { getTypeRgb } from "#data/type"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { ModifierTier } from "#enums/modifier-tier"; +import { HeldItemCategoryId, type HeldItemId, isItemInCategory } from "#enums/held-item-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { PokeballType } from "#enums/pokeball"; +import { RewardPoolType } from "#enums/reward-pool-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerType } from "#enums/trainer-type"; import { doShinySparkleAnim } from "#field/anims"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon"; -import type { PokemonHeldItemModifier } from "#modifiers/modifier"; -import { - HiddenAbilityRateBoosterModifier, - PokemonFormChangeItemModifier, - ShinyRateBoosterModifier, - SpeciesStatBoosterModifier, -} from "#modifiers/modifier"; -import type { ModifierTypeOption } from "#modifiers/modifier-type"; -import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; +import { getHeldItemTier } from "#items/held-item-default-tiers"; +import type { RewardOption } from "#items/reward"; +import { generatePlayerRewardOptions, generateRewardPoolWeights, getRewardPoolForType } from "#items/reward-pool-utils"; +import { isTmReward } from "#items/reward-utils"; +import { TrainerItemEffect } from "#items/trainer-item"; import { PokemonMove } from "#moves/pokemon-move"; import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { @@ -209,9 +206,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(); @@ -235,16 +232,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); @@ -278,7 +271,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil if (timedEventManager.isEventActive()) { shinyThreshold.value *= timedEventManager.getShinyMultiplier(); } - globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + globalScene.applyPlayerItems(TrainerItemEffect.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold }); // Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms // Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that @@ -292,7 +285,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil if (tradePokemon.species.abilityHidden) { if (tradePokemon.abilityIndex < hiddenIndex) { const hiddenAbilityChance = new NumberHolder(64); - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); @@ -331,9 +326,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(); @@ -356,16 +351,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); @@ -390,17 +381,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; }, @@ -411,10 +400,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; } @@ -426,44 +412,36 @@ 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) ?? RarityTier.GREAT; + // Increment tier by 1 - if (tier < ModifierTier.MASTER) { + if (tier < RarityTier.MASTER) { tier++; } - regenerateModifierPoolThresholds(party, ModifierPoolType.PLAYER, 0); - let item: ModifierTypeOption | null = null; - // TMs excluded from possible rewards - while (!item || item.type.id.includes("TM_")) { - item = getPlayerModifierTypeOptions(1, party, [], { - guaranteedModifierTiers: [tier], + generateRewardPoolWeights(getRewardPoolForType(RewardPoolType.PLAYER), party, 0); + let item: RewardOption | null = null; + // TMs excluded from possible allRewards + while (!item || isTmReward(item.type)) { + item = generatePlayerRewardOptions(1, party, [], { + guaranteedRarityTiers: [tier], allowLuckUpgrades: false, })[0]; } encounter.setDialogueToken("itemName", item.type.name); setEncounterRewards({ - guaranteedModifierTypeOptions: [item], + guaranteedRewardOptions: [item], fillRemaining: false, }); - chosenPokemon.loseHeldItem(modifier, false); - await globalScene.updateModifiers(true, true); + chosenPokemon.heldItemManager.remove(heldItemId); + await globalScene.updateItems(true); // Generate a trainer name const traderName = generateRandomTraderName(); @@ -484,7 +462,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index 6f15f150d8b..4d7a03485c7 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -1,10 +1,10 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; -import { ModifierTier } from "#enums/modifier-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { initBattleWithEnemyConfig, setEncounterRewards } from "#mystery-encounters/encounter-phase-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; @@ -147,7 +147,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], + guaranteedRewardSpecs: [RewardId.TM_COMMON, RewardId.TM_GREAT, RewardId.MEMORY_MUSHROOM], fillRemaining: true, }); @@ -175,7 +175,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], + guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.GREAT, RarityTier.GREAT], fillRemaining: true, }); @@ -206,7 +206,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter encounter.expMultiplier = 0.9; setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], + guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ULTRA, RarityTier.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 1bc2404dc27..ae526b6b4a3 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -1,10 +1,10 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { ModifierTier } from "#enums/modifier-tier"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; @@ -141,25 +141,25 @@ 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], + guaranteedRarityTiers: [RarityTier.COMMON, RarityTier.COMMON, RarityTier.GREAT, RarityTier.GREAT], }); - // Display result message then proceed to rewards + // Display result message then proceed to allRewards queueEncounterMessage(`${namespace}:option.1.normal`); leaveEncounterWithoutBattle(); } 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], + guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.ULTRA], }); - // Display result message then proceed to rewards + // Display result message then proceed to allRewards queueEncounterMessage(`${namespace}:option.1.good`); leaveEncounterWithoutBattle(); } 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], + guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE], }); - // Display result message then proceed to rewards + // Display result message then proceed to allRewards queueEncounterMessage(`${namespace}:option.1.great`); leaveEncounterWithoutBattle(); } else if ( @@ -168,9 +168,9 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde ) { // Choose 1 MASTER tier item (5%) setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.MASTER], + guaranteedRarityTiers: [RarityTier.MASTER], }); - // Display result message then proceed to rewards + // Display result message then proceed to allRewards queueEncounterMessage(`${namespace}:option.1.amazing`); leaveEncounterWithoutBattle(); } else { @@ -208,7 +208,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index c3400b41327..fe8254804ee 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -8,9 +8,10 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PlayerGender } from "#enums/player-gender"; import { PokeballType } from "#enums/pokeball"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { TrainerSlot } from "#enums/trainer-slot"; import type { EnemyPokemon } from "#field/pokemon"; -import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#modifiers/modifier"; +import { TrainerItemEffect } from "#items/trainer-item"; import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { initSubsequentOptionSelect, @@ -124,7 +125,7 @@ export const SafariZoneEncounter: MysteryEncounter = MysteryEncounterBuilder.wit ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, @@ -297,7 +298,9 @@ async function summonSafariPokemon() { const hiddenIndex = pokemon.species.ability2 ? 2 : 1; if (pokemon.abilityIndex < hiddenIndex) { const hiddenAbilityChance = new NumberHolder(256); - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); @@ -332,8 +335,7 @@ async function summonSafariPokemon() { // shows up and the IV scanner breaks. For now, we place the IV scanner code // separately so that at least the IV scanner works. - const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); - if (ivScannerModifier) { + if (globalScene.trainerItems.hasItem(TrainerItemId.IV_SCANNER)) { globalScene.phaseManager.pushNew("ScanIvsPhase", pokemon.getBattlerIndex()); } } diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 47317c12b50..4c5a7e7a222 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -1,6 +1,6 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; +import { allHeldItems } from "#data/data-lists"; import { getNatureName } from "#data/nature"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -8,9 +8,9 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { Nature } from "#enums/nature"; import { SpeciesId } from "#enums/species-id"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; +import { getNewVitaminHeldItem } from "#items/held-item-pool"; import { getEncounterText, queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import { - generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, @@ -18,7 +18,6 @@ import { } from "#mystery-encounters/encounter-phase-utils"; import { applyDamageToPokemon, - applyModifierTypeToPlayerPokemon, isPokemonValidForEncounterOptionSelection, } from "#mystery-encounters/encounter-pokemon-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; @@ -96,15 +95,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, }; }; @@ -131,10 +127,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); @@ -179,15 +175,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, }; }; @@ -202,10 +195,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); @@ -234,7 +227,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index cddfef1ef76..62f864bb464 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -1,9 +1,8 @@ import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; import { CustomPokemonData } from "#data/pokemon-data"; import { AiType } from "#enums/ai-type"; import { BattlerIndex } from "#enums/battler-index"; -import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveUseMode } from "#enums/move-use-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -11,14 +10,11 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Nature } from "#enums/nature"; import { SpeciesId } from "#enums/species-id"; -import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, @@ -78,24 +74,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 @@ -131,7 +115,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil // Pick battle const encounter = globalScene.currentBattle.mysteryEncounter!; setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], + guaranteedRewardSpecs: [HeldItemId.LEFTOVERS], fillRemaining: true, }); encounter.startOfBattleEffects.push({ @@ -178,7 +162,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil // Steal the Snorlax's Leftovers const instance = globalScene.currentBattle.mysteryEncounter!; setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], + guaranteedRewardSpecs: [HeldItemId.LEFTOVERS], fillRemaining: false, }); // Snorlax exp to Pokemon that did the stealing diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index d77326837cd..4579ad1246b 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -1,9 +1,9 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import { modifierTypes } from "#data/data-lists"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -13,11 +13,9 @@ import { TrainerSlot } from "#enums/trainer-slot"; import { getBiomeKey } from "#field/arena"; import type { Pokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon"; -import { getPartyLuckValue } from "#modifiers/modifier-type"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierTypeOption, initBattleWithEnemyConfig, setEncounterExp, setEncounterRewards, @@ -34,6 +32,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou import { MoneyRequirement, WaveModulusRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { PokemonData } from "#system/pokemon-data"; import { randSeedInt } from "#utils/common"; +import { getPartyLuckValue } from "#utils/party"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/teleportingHijinks"; @@ -173,10 +172,8 @@ export const TeleportingHijinksEncounter: MysteryEncounter = MysteryEncounterBui ], }; - const magnet = generateModifierTypeOption(modifierTypes.ATTACK_TYPE_BOOSTER, [PokemonType.STEEL])!; - const metalCoat = generateModifierTypeOption(modifierTypes.ATTACK_TYPE_BOOSTER, [PokemonType.ELECTRIC])!; setEncounterRewards({ - guaranteedModifierTypeOptions: [magnet, metalCoat], + guaranteedRewardSpecs: [HeldItemId.MAGNET, HeldItemId.METAL_COAT], fillRemaining: true, }); await transitionMysteryEncounterIntroVisuals(true, true); diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index 235bd322ef8..5e1e50e2c41 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -1,11 +1,11 @@ import { globalScene } from "#app/global-scene"; import { speciesStarterCosts } from "#balance/starters"; -import { modifierTypes } from "#data/data-lists"; import type { IEggOptions } from "#data/egg"; import { getPokeballTintColor } from "#data/pokeball"; import { BiomeId } from "#enums/biome-id"; import { EggSourceType } from "#enums/egg-source-types"; import { EggTier } from "#enums/egg-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -294,7 +294,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount const eggOptions = getEggOptions(pokemon1CommonEggs, pokemon1RareEggs); setEncounterRewards( { - guaranteedModifierTypeFuncs: [modifierTypes.SOOTHE_BELL], + guaranteedRewardSpecs: [HeldItemId.SOOTHE_BELL], fillRemaining: true, }, eggOptions, @@ -304,7 +304,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(encounter, pokemon1); - // Configure outro dialogue for egg rewards + // Configure outro dialogue for egg allRewards encounter.dialogue.outro = [ { speaker: trainerNameKey, @@ -353,7 +353,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount const eggOptions = getEggOptions(pokemon2CommonEggs, pokemon2RareEggs); setEncounterRewards( { - guaranteedModifierTypeFuncs: [modifierTypes.SOOTHE_BELL], + guaranteedRewardSpecs: [HeldItemId.SOOTHE_BELL], fillRemaining: true, }, eggOptions, @@ -363,7 +363,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(encounter, pokemon2); - // Configure outro dialogue for egg rewards + // Configure outro dialogue for egg allRewards encounter.dialogue.outro = [ { speaker: trainerNameKey, @@ -412,7 +412,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount const eggOptions = getEggOptions(pokemon3CommonEggs, pokemon3RareEggs); setEncounterRewards( { - guaranteedModifierTypeFuncs: [modifierTypes.SOOTHE_BELL], + guaranteedRewardSpecs: [HeldItemId.SOOTHE_BELL], fillRemaining: true, }, eggOptions, @@ -422,7 +422,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(encounter, pokemon3); - // Configure outro dialogue for egg rewards + // Configure outro dialogue for egg allRewards encounter.dialogue.outro = [ { speaker: trainerNameKey, @@ -614,7 +614,6 @@ function removePokemonFromPartyAndStoreHeldItems(encounter: MysteryEncounter, ch party[chosenIndex] = party[0]; party[0] = chosenPokemon; encounter.misc.originalParty = globalScene.getPlayerParty().slice(1); - encounter.misc.originalPartyHeldItems = encounter.misc.originalParty.map(p => p.getHeldItems()); globalScene["party"] = [chosenPokemon]; } @@ -623,14 +622,7 @@ function restorePartyAndHeldItems() { // Restore original party globalScene.getPlayerParty().push(...encounter.misc.originalParty); - // Restore held items - const originalHeldItems = encounter.misc.originalPartyHeldItems; - for (const pokemonHeldItemsList of originalHeldItems) { - for (const heldItem of pokemonHeldItemsList) { - globalScene.addModifier(heldItem, true, false, false, true); - } - } - globalScene.updateModifiers(true); + globalScene.updateItems(true); } function onGameOver() { @@ -648,7 +640,7 @@ function onGameOver() { const chosenPokemon = encounter.misc.chosenPokemon; chosenPokemon.friendship = 0; - // Clear all rewards that would have been earned + // Clear all allRewards that would have been earned encounter.doEncounterRewards = undefined; // Set flag that encounter was failed diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 91662993a51..539e1a3670b 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -238,7 +238,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, 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 568dc5de8b1..3ce4aeb6412 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -1,10 +1,9 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; import { CustomPokemonData } from "#data/pokemon-data"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveUseMode } from "#enums/move-use-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -13,19 +12,16 @@ import { Nature } from "#enums/nature"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import type { Pokemon } from "#field/pokemon"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#mystery-encounters/encounter-phase-utils"; -import { modifyPlayerPokemonBST } from "#mystery-encounters/encounter-pokemon-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { getPokemonSpecies } from "#utils/pokemon-utils"; @@ -95,23 +91,12 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), nature: Nature.HARDY, moveSet: [MoveId.INFESTATION, MoveId.SALT_CURE, MoveId.GASTRO_ACID, MoveId.HEAL_ORDER], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, - stackCount: 2, - }, + heldItemConfig: [ + { entry: HeldItemId.SITRUS_BERRY, count: 1 }, + { entry: HeldItemId.ENIGMA_BERRY, count: 1 }, + { entry: HeldItemId.APICOT_BERRY, count: 1 }, + { entry: HeldItemId.GANLON_BERRY, count: 1 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ], tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { @@ -171,11 +156,11 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder sortedParty.forEach((pokemon, index) => { if (index < 2) { // -15 to the two highest BST mons - modifyPlayerPokemonBST(pokemon, false); + pokemon.heldItemManager.add(HeldItemId.SHUCKLE_JUICE_BAD); encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender()); } else { // +10 for the rest - modifyPlayerPokemonBST(pokemon, true); + pokemon.heldItemManager.add(HeldItemId.SHUCKLE_JUICE_GOOD); } }); @@ -207,7 +192,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder // Pick battle const encounter = globalScene.currentBattle.mysteryEncounter!; setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.SOUL_DEW], + guaranteedRewardSpecs: [HeldItemId.SOUL_DEW], fillRemaining: true, }); encounter.startOfBattleEffects.push( 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 e17bf0575d7..e6754a5b4bb 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -1,27 +1,25 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; +import { allRewards } from "#data/data-lists"; import { SpeciesFormChangeAbilityTrigger } from "#data/form-change-triggers"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; -import { ModifierTier } from "#enums/modifier-tier"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Nature } from "#enums/nature"; -import { PokemonType } from "#enums/pokemon-type"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; -import { Stat } from "#enums/stat"; import { TrainerType } from "#enums/trainer-type"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; +import type { Reward } from "#items/reward"; +import { generateRewardOptionFromId } from "#items/reward-utils"; import { showEncounterDialogue, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, - generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, @@ -32,6 +30,8 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; +// TODO: make all items unstealable + /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theWinstrateChallenge"; @@ -119,7 +119,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter = MysteryEncounter ], }, async () => { - // Spawn 5 trainer battles back to back with Macho Brace in rewards + // Spawn 5 trainer battles back to back with Macho Brace in allRewards globalScene.currentBattle.mysteryEncounter!.doContinueEncounter = async () => { await endTrainerBattleAndShowDialogue(); }; @@ -142,7 +142,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter = MysteryEncounter // Refuse the challenge, they full heal the party and give the player a Rarer Candy globalScene.phaseManager.unshiftNew("PartyHealPhase", true); setEncounterRewards({ - guaranteedModifierTypeFuncs: [modifierTypes.RARER_CANDY], + guaranteedRewardSpecs: [RewardId.RARER_CANDY], fillRemaining: false, }); leaveEncounterWithoutBattle(); @@ -158,17 +158,17 @@ async function spawnNextTrainerOrEndEncounter() { await showEncounterDialogue(`${namespace}:victory`, `${namespace}:speaker`); // Give 10x Voucher - const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier(); - globalScene.addModifier(newModifier); + const reward = allRewards[RewardId.VOUCHER_PREMIUM](); + globalScene.applyReward(reward as Reward, {}); globalScene.playSound("item_fanfare"); - await showEncounterText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); + await showEncounterText(i18next.t("battle:rewardGain", { modifierName: (reward as Reward).name })); 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; + globalScene.ui.clearText(); // Clears "Winstrate" title from screen as allRewards get animated in + const machoBrace = generateRewardOptionFromId(HeldItemId.MACHO_BRACE)!; + machoBrace.type.tier = RarityTier.MASTER; setEncounterRewards({ - guaranteedModifierTypeOptions: [machoBrace], + guaranteedRewardOptions: [machoBrace], fillRemaining: false, }); encounter.doContinueEncounter = undefined; @@ -258,16 +258,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 +269,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 +288,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 +299,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 +318,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 +329,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 +340,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 +356,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 +372,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 +380,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 +400,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 +408,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 +416,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 393f8a24e51..af3766e08ae 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -12,7 +12,6 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Nature } from "#enums/nature"; import { getStatKey } from "#enums/stat"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; -import type { PokemonHeldItemModifier } from "#modifiers/modifier"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import { @@ -26,7 +25,6 @@ import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { PokemonData } from "#system/pokemon-data"; -import type { HeldModifierConfig } from "#types/held-modifier-config"; import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; import { isNullOrUndefined, randSeedShuffle } from "#utils/common"; import { getEnumValues } from "#utils/enums"; @@ -102,8 +100,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Spawn light training session with chosen pokemon // Every 50 waves, add +1 boss segment, capping at 5 const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 50), 5); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(playerPokemon, segments, modifiers); + const config = getEnemyConfig(playerPokemon, segments); globalScene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { @@ -152,13 +149,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde globalScene.gameData.setPokemonCaught(playerPokemon, false); } - // Add pokemon and mods back - globalScene.getPlayerParty().push(playerPokemon); - for (const mod of modifiers.value) { - mod.pokemonId = playerPokemon.id; - globalScene.addModifier(mod, true, false, false, true); - } - globalScene.updateModifiers(true); + // Make held items show up again + globalScene.updateItems(true); queueEncounterMessage(`${namespace}:option.1.finished`); }; @@ -217,8 +209,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Spawn medium training session with chosen pokemon // Every 40 waves, add +1 boss segment, capping at 6 const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 40), 6); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(playerPokemon, segments, modifiers); + const config = getEnemyConfig(playerPokemon, segments); globalScene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { @@ -227,13 +218,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde playerPokemon.setCustomNature(encounter.misc.chosenNature); globalScene.gameData.unlockSpeciesNature(playerPokemon.species, encounter.misc.chosenNature); - // Add pokemon and modifiers back - globalScene.getPlayerParty().push(playerPokemon); - for (const mod of modifiers.value) { - mod.pokemonId = playerPokemon.id; - globalScene.addModifier(mod, true, false, false, true); - } - globalScene.updateModifiers(true); + // Make held items show up again + globalScene.updateItems(true); }; setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase); @@ -308,8 +294,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Every 30 waves, add +1 boss segment, capping at 6 // Also starts with +1 to all stats const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 30), 6); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(playerPokemon, segments, modifiers); + const config = getEnemyConfig(playerPokemon, segments); config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; globalScene.removePokemonFromPlayerParty(playerPokemon, false); @@ -340,13 +325,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde playerPokemon.calculateStats(); globalScene.gameData.setPokemonCaught(playerPokemon, false); - // Add pokemon and mods back - globalScene.getPlayerParty().push(playerPokemon); - for (const mod of modifiers.value) { - mod.pokemonId = playerPokemon.id; - globalScene.addModifier(mod, true, false, false, true); - } - globalScene.updateModifiers(true); + // Make held items show up again + globalScene.updateItems(true); }; setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase); @@ -366,25 +346,19 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde ], }, async () => { - // Leave encounter with no rewards or exp + // Leave encounter with no allRewards or exp leaveEncounterWithoutBattle(true); return true; }, ) .build(); -function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number, modifiers: ModifiersHolder): EnemyPartyConfig { +function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number): EnemyPartyConfig { playerPokemon.resetSummonData(); // Passes modifiers by reference - modifiers.value = playerPokemon.getHeldItems(); - const modifierConfigs = modifiers.value.map(mod => { - return { - modifier: mod.clone(), - isTransferable: false, - stackCount: mod.stackCount, - }; - }) as HeldModifierConfig[]; + // TODO: fix various things, like make enemy items untransferable, make sure form change items can come back + const config = playerPokemon.heldItemManager.generateHeldItemConfiguration(); const data = new PokemonData(playerPokemon); return { @@ -396,12 +370,8 @@ function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number, modifier formIndex: playerPokemon.formIndex, level: playerPokemon.level, dataSource: data, - modifierConfigs: modifierConfigs, + heldItemConfig: config, }, ], }; } - -class ModifiersHolder { - public value: PokemonHeldItemModifier[] = []; -} diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index 452a9a8bb4b..c0b87645219 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -1,28 +1,27 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; +import { allHeldItems, allTrainerItems } from "#data/data-lists"; import { BattlerIndex } from "#enums/battler-index"; -import { ModifierTier } from "#enums/modifier-tier"; +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveUseMode } from "#enums/move-use-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; -import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#modifiers/modifier"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { assignItemToFirstFreePokemon } from "#items/item-utility"; import { PokemonMove } from "#moves/pokemon-move"; import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#mystery-encounters/encounter-phase-utils"; -import { applyModifierTypeToPlayerPokemon } from "#mystery-encounters/encounter-pokemon-utils"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; @@ -83,41 +82,13 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde formIndex: 1, // Gmax bossSegmentModifier: 1, // +1 Segment from normal moveSet: [MoveId.GUNK_SHOT, MoveId.STOMPING_TANTRUM, MoveId.HAMMER_ARM, MoveId.PAYBACK], - modifierConfigs: [ - { - modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType, - }, - { - modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType, - stackCount: randSeedInt(2, 0), - }, - { - modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType, - stackCount: randSeedInt(2, 1), - }, - { - modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType, - stackCount: randSeedInt(3, 1), - }, - { - modifier: generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType, - stackCount: randSeedInt(2, 0), - }, + heldItemConfig: [ + { entry: HeldItemCategoryId.BERRY, count: 4 }, + { entry: HeldItemCategoryId.BASE_STAT_BOOST, count: 2 }, + { entry: HeldItemId.TOXIC_ORB, count: randSeedInt(2, 0) }, + { entry: HeldItemId.SOOTHE_BELL, count: randSeedInt(2, 1) }, + { entry: HeldItemId.LUCKY_EGG, count: randSeedInt(3, 1) }, + { entry: HeldItemId.GOLDEN_EGG, count: randSeedInt(2, 0) }, ], }; const config: EnemyPartyConfig = { @@ -157,18 +128,14 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde await transitionMysteryEncounterIntroVisuals(); await tryApplyDigRewardItems(); - const blackSludge = generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ - SHOP_ITEM_COST_MULTIPLIER, - ]); - const modifier = blackSludge?.newModifier(); - if (modifier) { - await globalScene.addModifier(modifier, false, false, false, true); + const blackSludge = globalScene.trainerItems.add(TrainerItemId.BLACK_SLUDGE); + if (blackSludge) { globalScene.playSound("battle_anims/PRSFX- Venom Drench", { volume: 2, }); await showEncounterText( i18next.t("battle:rewardGain", { - modifierName: modifier.type.name, + modifierName: allTrainerItems[TrainerItemId.BLACK_SLUDGE].name, }), null, undefined, @@ -200,7 +167,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde const encounter = globalScene.currentBattle.mysteryEncounter!; setEncounterRewards({ - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], + guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ULTRA, RarityTier.GREAT], fillRemaining: true, }); encounter.startOfBattleEffects.push( @@ -224,44 +191,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 +211,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 c9e2ffedeec..52986bec62e 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -2,6 +2,7 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemCategoryId, type HeldItemId } from "#enums/held-item-id"; import type { MoveId } from "#enums/move-id"; import { MoveUseMode } from "#enums/move-use-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -10,7 +11,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PokeballType } from "#enums/pokeball"; import { Stat } from "#enums/stat"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; -import { BerryModifier } from "#modifiers/modifier"; +import type { HeldItemSpecs } from "#items/held-item-data-types"; +import { getPartyBerries } from "#items/item-utility"; import { PokemonMove } from "#moves/pokemon-move"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; @@ -29,10 +31,10 @@ import { import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; -import { MoveRequirement, PersistentModifierRequirement } from "#mystery-encounters/mystery-encounter-requirements"; +import { HeldItemRequirement, MoveRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { CHARMING_MOVES } from "#mystery-encounters/requirement-groups"; import { PokemonData } from "#system/pokemon-data"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#utils/common"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/uncommonBreed"; @@ -187,7 +189,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`, @@ -202,20 +204,16 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. // Give it some food // 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(); + 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 berryWeights = berryMap.map(b => (b.item as HeldItemSpecs).stack); + const index = pickWeightedIndex(berryWeights) ?? 0; + const randBerry = berryMap[index]; + globalScene.getPokemonById(randBerry.pokemonId)?.heldItemManager.remove(randBerry.item.id as HeldItemId); + (randBerry.item as HeldItemSpecs).stack -= 1; } - await globalScene.updateModifiers(true, true); + await globalScene.updateItems(true); // Pokemon joins the team, with 2 egg moves const encounter = globalScene.currentBattle.mysteryEncounter!; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 1164d2ca7ca..b172748f1ef 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -1,9 +1,9 @@ import { globalScene } from "#app/global-scene"; -import { allSpecies, modifierTypes } from "#data/data-lists"; +import { allSpecies } from "#data/data-lists"; import { getLevelTotalExp } from "#data/exp"; import type { PokemonSpecies } from "#data/pokemon-species"; import { Challenges } from "#enums/challenges"; -import { ModifierTier } from "#enums/modifier-tier"; +import { HeldItemId } from "#enums/held-item-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -11,17 +11,17 @@ import { Nature } from "#enums/nature"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { PlayerGender } from "#enums/player-gender"; import { PokemonType } from "#enums/pokemon-type"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; -import type { PokemonHeldItemModifier } from "#modifiers/modifier"; -import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#modifiers/modifier"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; +import type { HeldItemConfiguration } from "#items/held-item-data-types"; +import { TrainerItemEffect } from "#items/trainer-item"; import { PokemonMove } from "#moves/pokemon-move"; import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils"; import { - generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, @@ -38,7 +38,6 @@ import { achvs } from "#system/achv"; import { PokemonData } from "#system/pokemon-data"; import { trainerConfigs } from "#trainers/trainer-config"; import { TrainerPartyTemplate } from "#trainers/trainer-party-template"; -import type { HeldModifierConfig } from "#types/held-modifier-config"; import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; @@ -220,12 +219,12 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit await doNewTeamPostProcess(transformations); setEncounterRewards({ - guaranteedModifierTypeFuncs: [ - modifierTypes.MEMORY_MUSHROOM, - modifierTypes.ROGUE_BALL, - modifierTypes.MINT, - modifierTypes.MINT, - modifierTypes.MINT, + guaranteedRewardSpecs: [ + RewardId.MEMORY_MUSHROOM, + RewardId.ROGUE_BALL, + RewardId.MINT, + RewardId.MINT, + RewardId.MINT, ], fillRemaining: false, }); @@ -244,7 +243,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit ], }, async () => { - // Battle your "future" team for some item rewards + // Battle your "future" team for some item RewardId const transformations: PokemonTransformation[] = globalScene.currentBattle.mysteryEncounter!.misc.teamTransformations; @@ -260,20 +259,14 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit dataSource.player = false; // Copy held items to new pokemon - const newPokemonHeldItemConfigs: HeldModifierConfig[] = []; - for (const item of transformation.heldItems) { - newPokemonHeldItemConfigs.push({ - modifier: item.clone() as PokemonHeldItemModifier, - stackCount: item.getStackCount(), - isTransferable: false, - }); - } + // TODO: Make items untransferable + const newPokemonHeldItemConfig = transformation.heldItems; + // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats if (shouldGetOldGateau(newPokemon)) { - newPokemonHeldItemConfigs.push({ - modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU) as PokemonHeldItemModifierType, - stackCount: 1, - isTransferable: false, + newPokemonHeldItemConfig.push({ + entry: HeldItemId.OLD_GATEAU, + count: 1, }); } @@ -282,7 +275,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); @@ -301,7 +294,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit }; const onBeforeRewards = () => { - // Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently) + // Before battle RewardId, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently) // One random pokemon will get its passive unlocked const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { @@ -314,13 +307,13 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit setEncounterRewards( { - guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.GREAT, - ModifierTier.GREAT, + guaranteedRarityTiers: [ + RarityTier.ROGUE, + RarityTier.ROGUE, + RarityTier.ULTRA, + RarityTier.ULTRA, + RarityTier.GREAT, + RarityTier.GREAT, ], fillRemaining: false, }, @@ -364,7 +357,7 @@ interface PokemonTransformation { previousPokemon: PlayerPokemon; newSpecies: PokemonSpecies; newPokemon: PlayerPokemon; - heldItems: PokemonHeldItemModifier[]; + heldItems: HeldItemConfiguration; } function getTeamTransformations(): PokemonTransformation[] { @@ -389,9 +382,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]; @@ -447,17 +438,14 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) { } // Copy old items to new pokemon - for (const item of transformation.heldItems) { - item.pokemonId = newPokemon.id; - globalScene.addModifier(item, false, false, false, true); - } + const heldItemConfiguration = transformation.heldItems; + // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats if (shouldGetOldGateau(newPokemon)) { - const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU(); - const modifier = modType?.newModifier(newPokemon); - if (modifier) { - globalScene.addModifier(modifier, false, false, false, true); - } + heldItemConfiguration.push({ + entry: HeldItemId.OLD_GATEAU, + count: 1, + }); } newPokemon.calculateStats(); @@ -499,7 +487,9 @@ async function postProcessTransformedPokemon( const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; if (newPokemon.abilityIndex < hiddenIndex) { const hiddenAbilityChance = new NumberHolder(256); - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index d71964db4b8..99741a6fdbf 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,9 +1,10 @@ import { globalScene } from "#app/global-scene"; -import { allAbilities } from "#data/data-lists"; +import { allAbilities, allHeldItems } from "#data/data-lists"; import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; import { pokemonFormChanges } from "#data/pokemon-forms"; import type { AbilityId } from "#enums/ability-id"; import { FormChangeItem } from "#enums/form-change-item"; +import { getHeldItemCategory, type HeldItemCategoryId, type HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Nature } from "#enums/nature"; @@ -13,8 +14,6 @@ import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; import { WeatherType } from "#enums/weather-type"; import type { PlayerPokemon } from "#field/pokemon"; -import { AttackTypeBoosterModifier } from "#modifiers/modifier"; -import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type"; import { coerceArray, isNullOrUndefined } from "#utils/common"; export interface EncounterRequirement { @@ -351,39 +350,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 @@ -832,73 +798,14 @@ 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[]; +export class HoldingItemRequirement extends EncounterPokemonRequirement { + requiredHeldItems: (HeldItemId | HeldItemCategoryId)[]; minNumberOfPokemon: number; invertQuery: boolean; requireTransferable: boolean; constructor( - heldItemTypes: PokemonType | PokemonType[], + heldItem: HeldItemId | HeldItemCategoryId | (HeldItemId | HeldItemCategoryId)[], minNumberOfPokemon = 1, invertQuery = false, requireTransferable = true, @@ -906,7 +813,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,45 +828,92 @@ 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 => { + return this.requireTransferable + ? pokemon.heldItemManager.hasTransferableItem(heldItem) + : pokemon.heldItemManager.hasItem(heldItem); }), ); } // 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.requiredHeldItemTypes.some( - heldItemType => - it instanceof AttackTypeBoosterModifier && - (it.type as AttackTypeBoosterModifierType).moveType === heldItemType && - (!this.requireTransferable || it.isTransferable), - ); - }).length > 0, + return partyPokemon.filter(pokemon => + pokemon.getHeldItems().some(item => { + return ( + !this.requiredHeldItems.some(heldItem => item === heldItem || getHeldItemCategory(item) === heldItem) && + (!this.requireTransferable || allHeldItems[item].isTransferable) + ); + }), ); } 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", ""]; + } +} + +export class HeldItemRequirement extends EncounterSceneRequirement { + requiredHeldItems: (HeldItemId | HeldItemCategoryId)[]; + minNumberOfItems: number; + invertQuery: boolean; + requireTransferable: boolean; + + constructor( + heldItem: HeldItemId | HeldItemCategoryId | (HeldItemId | HeldItemCategoryId)[], + minNumberOfItems = 1, + invertQuery = false, + requireTransferable = true, + ) { + super(); + this.minNumberOfItems = minNumberOfItems; + this.invertQuery = invertQuery; + this.requiredHeldItems = coerceArray(heldItem); + this.requireTransferable = requireTransferable; + } + + override meetsRequirement(): boolean { + const partyPokemon = globalScene.getPlayerParty(); + if (isNullOrUndefined(partyPokemon)) { + return false; + } + console.log("COUNTED:", this.queryPartyForItems(partyPokemon), this.minNumberOfItems); + return this.queryPartyForItems(partyPokemon) >= this.minNumberOfItems; + } + + queryPartyForItems(partyPokemon: PlayerPokemon[]): number { + let count = 0; + for (const pokemon of partyPokemon) { + for (const item of pokemon.getHeldItems()) { + const itemInList = this.requiredHeldItems.some( + heldItem => item === heldItem || getHeldItemCategory(item) === heldItem, + ); + const requiredItem = this.invertQuery ? !itemInList : itemInList; + if (requiredItem && (!this.requireTransferable || allHeldItems[item].isTransferable)) { + count += pokemon.heldItemManager.getStack(item); + } + } + } + return count; + } + + override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { + const requiredItems = pokemon?.getHeldItems().filter(item => { + return ( + this.requiredHeldItems.some(heldItem => item === heldItem) && + (!this.requireTransferable || allHeldItems[item].isTransferable) + ); + }); + if (requiredItems && requiredItems.length > 0) { + return ["heldItem", allHeldItems[requiredItems[0]].name]; } return ["heldItem", ""]; } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 47dfe58cace..522a62d2def 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -174,11 +174,11 @@ export class MysteryEncounter implements IMysteryEncounter { onVisualsStart?: () => boolean; /** Event triggered prior to {@linkcode CommandPhase}, during {@linkcode TurnInitPhase} */ onTurnStart?: () => boolean; - /** Event prior to any rewards logic in {@linkcode MysteryEncounterRewardsPhase} */ + /** Event prior to any allRewards logic in {@linkcode MysteryEncounterRewardsPhase} */ onRewards?: () => Promise; - /** Will provide the player party EXP before rewards are displayed for that wave */ + /** Will provide the player party EXP before allRewards are displayed for that wave */ doEncounterExp?: () => boolean; - /** Will provide the player a rewards shop for that wave */ + /** Will provide the player a allRewards shop for that wave */ doEncounterRewards?: () => boolean; /** Will execute callback during VictoryPhase of a continuousEncounter */ doContinueEncounter?: () => Promise; @@ -238,10 +238,10 @@ export class MysteryEncounter implements IMysteryEncounter { encounterMode: MysteryEncounterMode; /** * Flag for checking if it's the first time a shop is being shown for an encounter. - * Defaults to true so that the first shop does not override the specified rewards. + * Defaults to true so that the first shop does not override the specified allRewards. * Will be set to false after a shop is shown (so can't reroll same rarity items for free) */ - lockEncounterRewardTiers: boolean; + lockEncounterRarityTiers: boolean; /** * Will be set automatically, indicates special moves in startOfBattleEffects are complete (so will not repeat) */ @@ -296,7 +296,7 @@ export class MysteryEncounter implements IMysteryEncounter { // Reset any dirty flags or encounter data this.startOfBattleEffectsComplete = false; - this.lockEncounterRewardTiers = true; + this.lockEncounterRarityTiers = true; this.dialogueTokens = {}; this.enemyPartyConfigs = []; this.startOfBattleEffects = []; @@ -562,7 +562,7 @@ export class MysteryEncounterBuilder implements Partial { continuousEncounter = false; catchAllowed = false; fleeAllowed = true; - lockEncounterRewardTiers = false; + lockEncounterRarityTiers = false; startOfBattleEffectsComplete = false; hasBattleAnimationsWithoutTargets = false; skipEnemyBattleTurns = false; @@ -929,13 +929,13 @@ export class MysteryEncounterBuilder implements Partial { } /** - * Can set custom encounter rewards via this callback function - * If rewards are always deterministic for an encounter, this is a good way to set them + * Can set custom encounter allRewards via this callback function + * If allRewards are always deterministic for an encounter, this is a good way to set them * - * NOTE: If rewards are dependent on options selected, runtime data, etc., + * NOTE: If allRewards are dependent on options selected, runtime data, etc., * It may be better to programmatically set doEncounterRewards elsewhere. - * There is a helper function in mystery-encounter utils, setEncounterRewards(), which can be called programmatically to set rewards - * @param doEncounterRewards Synchronous callback function to perform during rewards phase of the encounter + * There is a helper function in mystery-encounter utils, setEncounterRewards(), which can be called programmatically to set allRewards + * @param doEncounterRewards Synchronous callback function to perform during allRewards phase of the encounter * @returns */ withRewards(doEncounterRewards: () => boolean): this & Required> { @@ -946,10 +946,10 @@ export class MysteryEncounterBuilder implements Partial { * Can set custom encounter exp via this callback function * If exp always deterministic for an encounter, this is a good way to set them * - * NOTE: If rewards are dependent on options selected, runtime data, etc., + * NOTE: If allRewards are dependent on options selected, runtime data, etc., * It may be better to programmatically set doEncounterExp elsewhere. - * There is a helper function in mystery-encounter utils, setEncounterExp(), which can be called programmatically to set rewards - * @param doEncounterExp Synchronous callback function to perform during rewards phase of the encounter + * There is a helper function in mystery-encounter utils, setEncounterExp(), which can be called programmatically to set allRewards + * @param doEncounterExp Synchronous callback function to perform during allRewards phase of the encounter * @returns */ withExp(doEncounterExp: () => boolean): this & Required> { diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 6b085978b27..48968a2ff82 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -5,7 +5,6 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { BiomePoolTier, biomeLinks } from "#balance/biomes"; import { initMoveAnim, loadMoveAnimAssets } from "#data/battle-anims"; -import { modifierTypes } from "#data/data-lists"; import type { IEggOptions } from "#data/egg"; import { Egg } from "#data/egg"; import type { Gender } from "#data/gender"; @@ -14,11 +13,9 @@ import type { CustomPokemonData } from "#data/pokemon-data"; import type { PokemonSpecies } from "#data/pokemon-species"; import { Status } from "#data/status-effect"; import type { AiType } from "#enums/ai-type"; -import { BattleType } from "#enums/battle-type"; import type { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; import { FieldPosition } from "#enums/field-position"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; import type { MoveId } from "#enums/move-id"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import type { Nature } from "#enums/nature"; @@ -31,13 +28,8 @@ import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon"; import { Trainer } from "#field/trainer"; -import type { CustomModifierSettings, ModifierType } from "#modifiers/modifier-type"; -import { - getPartyLuckValue, - ModifierTypeGenerator, - ModifierTypeOption, - regenerateModifierPoolThresholds, -} from "#modifiers/modifier-type"; +import type { HeldItemConfiguration } from "#items/held-item-data-types"; +import type { CustomRewardSettings } from "#items/reward-pool-utils"; import { PokemonMove } from "#moves/pokemon-move"; import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { MysteryEncounterOption } from "#mystery-encounters/mystery-encounter-option"; @@ -45,11 +37,11 @@ import type { Variant } from "#sprites/variant"; import type { PokemonData } from "#system/pokemon-data"; import type { TrainerConfig } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config"; -import type { HeldModifierConfig } from "#types/held-modifier-config"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler"; import { PartyUiMode } from "#ui/party-ui-handler"; import { coerceArray, isNullOrUndefined, randomString, randSeedInt, randSeedItem } from "#utils/common"; +import { getPartyLuckValue } from "#utils/party"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -101,7 +93,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; @@ -199,6 +191,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): battle.enemyLevels.forEach((level, e) => { let enemySpecies: PokemonSpecies | undefined; + let heldItemConfig: HeldItemConfiguration = []; let dataSource: PokemonData | undefined; let isBoss = false; if (!loaded) { @@ -210,12 +203,14 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): dataSource = config.dataSource; enemySpecies = config.species; isBoss = config.isBoss; + heldItemConfig = config.heldItemConfig ?? []; battle.enemyParty[e] = globalScene.addEnemyPokemon( enemySpecies, level, TrainerSlot.TRAINER, isBoss, false, + heldItemConfig, dataSource, ); } else { @@ -225,6 +220,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { const config = partyConfig.pokemonConfigs[e]; level = config.level ? config.level : level; + heldItemConfig = config.heldItemConfig ?? []; dataSource = config.dataSource; enemySpecies = config.species; isBoss = config.isBoss; @@ -241,6 +237,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): TrainerSlot.NONE, isBoss, false, + heldItemConfig, dataSource, ); } @@ -427,16 +424,6 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): enemyPokemon_2.x += 300; } }); - if (!loaded) { - regenerateModifierPoolThresholds( - globalScene.getEnemyField(), - battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, - ); - const customModifierTypes = partyConfig?.pokemonConfigs - ?.filter(config => config?.modifierConfigs) - .map(config => config.modifierConfigs!); - globalScene.generateEnemyModifiers(customModifierTypes); - } } /** @@ -485,45 +472,6 @@ export function updatePlayerMoney(changeValue: number, playSound = true, showMes } } -/** - * Converts modifier bullshit to an actual item - * @param modifier - * @param pregenArgs Can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. - */ -export function generateModifierType(modifier: () => ModifierType, pregenArgs?: any[]): ModifierType | null { - const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); - if (!modifierId) { - return null; - } - - let result: ModifierType = modifierTypes[modifierId](); - - // Populates item id and tier (order matters) - result = result - .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(ModifierPoolType.PLAYER, globalScene.getPlayerParty()); - - return result instanceof ModifierTypeGenerator - ? result.generateType(globalScene.getPlayerParty(), pregenArgs) - : result; -} - -/** - * Converts modifier bullshit to an actual item - * @param modifier - * @param pregenArgs - can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. - */ -export function generateModifierTypeOption( - modifier: () => ModifierType, - pregenArgs?: any[], -): ModifierTypeOption | null { - const result = generateModifierType(modifier, pregenArgs); - if (result) { - return new ModifierTypeOption(result, 0); - } - return result; -} - /** * This function is intended for use inside onPreOptionPhase() of an encounter option * @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen @@ -738,12 +686,12 @@ export function selectOptionThenPokemon( /** * Will initialize reward phases to follow the mystery encounter * Can have shop displayed or skipped - * @param customShopRewards - adds a shop phase with the specified rewards / reward tiers + * @param customShopRewards - adds a shop phase with the specified allRewards / reward tiers * @param eggRewards * @param preRewardsCallback - can execute an arbitrary callback before the new phases if necessary (useful for updating items/party/injecting new phases before {@linkcode MysteryEncounterRewardsPhase}) */ export function setEncounterRewards( - customShopRewards?: CustomModifierSettings, + customShopRewards?: CustomRewardSettings, eggRewards?: IEggOptions[], preRewardsCallback?: Function, ) { @@ -753,7 +701,7 @@ export function setEncounterRewards( } if (customShopRewards) { - globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards); + globalScene.phaseManager.unshiftNew("SelectRewardPhase", 0, undefined, customShopRewards); } else { globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase")); } @@ -820,8 +768,8 @@ export function initSubsequentOptionSelect(optionSelectSettings: OptionSelectSet /** * Can be used to exit an encounter without any battles or followup - * Will skip any shops and rewards, and queue the next encounter phase as normal - * @param addHealPhase - when true, will add a shop phase to end of encounter with 0 rewards but healing items are available + * Will skip any shops and allRewards, and queue the next encounter phase as normal + * @param addHealPhase - when true, will add a shop phase to end of encounter with 0 allRewards but healing items are available * @param encounterMode - Can set custom encounter mode if necessary (may be required for forcing Pokemon to return before next phase) */ export function leaveEncounterWithoutBattle( diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 19f06707257..0f5b763166e 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 { getPokemonNameWithAffix } from "#app/messages"; import { speciesStarterCosts } from "#balance/starters"; -import { modifierTypes } from "#data/data-lists"; import { Gender } from "#data/gender"; import { doPokeballBounceAnim, @@ -13,6 +12,7 @@ import { CustomPokemonData } from "#data/pokemon-data"; import type { PokemonSpecies } from "#data/pokemon-species"; import { getStatusEffectCatchRateMultiplier } from "#data/status-effect"; import type { AbilityId } from "#enums/ability-id"; +import type { HeldItemId } from "#enums/held-item-id"; import { PlayerGender } from "#enums/player-gender"; import type { PokeballType } from "#enums/pokeball"; import type { PokemonType } from "#enums/pokemon-type"; @@ -22,8 +22,6 @@ import { StatusEffect } from "#enums/status-effect"; import { UiMode } from "#enums/ui-mode"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#field/anims"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; -import { PokemonHeldItemModifier } from "#modifiers/modifier"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import { getEncounterText, queueEncounterMessage, @@ -372,60 +370,13 @@ export function applyHealToPokemon(pokemon: PlayerPokemon, heal: number) { applyHpChangeToPokemon(pokemon, heal); } -/** - * Will modify all of a Pokemon's base stats by a flat value - * Base stats can never go below 1 - * @param pokemon - * @param value - */ -export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, good: boolean) { - const modType = modifierTypes - .MYSTERY_ENCOUNTER_SHUCKLE_JUICE() - .generateType(globalScene.getPlayerParty(), [good ? 10 : -15]) - ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); - const modifier = modType?.newModifier(pokemon); - if (modifier) { - globalScene.addModifier(modifier, false, false, false, true); - pokemon.calculateStats(); +export function applyHeldItemWithFallback(pokemon: Pokemon, item: HeldItemId, fallbackItem?: HeldItemId) { + const added = pokemon.heldItemManager.add(item); + if (!added && fallbackItem) { + pokemon.heldItemManager.add(fallbackItem); } } -/** - * Will attempt to add a new modifier to a Pokemon. - * If the Pokemon already has max stacks of that item, it will instead apply 'fallbackModifierType', if specified. - * @param scene - * @param pokemon - * @param modType - * @param fallbackModifierType - */ -export async function applyModifierTypeToPlayerPokemon( - pokemon: PlayerPokemon, - modType: PokemonHeldItemModifierType, - fallbackModifierType?: PokemonHeldItemModifierType, -) { - // Check if the Pokemon has max stacks of that item already - const modifier = modType.newModifier(pokemon); - const existing = globalScene.findModifier( - (m): m is PokemonHeldItemModifier => - m instanceof PokemonHeldItemModifier && - m.type.id === modType.id && - m.pokemonId === pokemon.id && - m.matchType(modifier), - ) as PokemonHeldItemModifier | undefined; - - // At max stacks - if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { - if (!fallbackModifierType) { - return; - } - - // Apply fallback - return applyModifierTypeToPlayerPokemon(pokemon, fallbackModifierType); - } - - globalScene.addModifier(modifier, false, false, false, true); -} - /** * Alternative to using AttemptCapturePhase * Assumes player sprite is visible on the screen (this is intended for non-combat uses) @@ -690,20 +641,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) { @@ -728,6 +669,7 @@ export async function catchPokemon( pokemon.variant, pokemon.ivs, pokemon.nature, + pokemon.heldItemManager.generateHeldItemConfiguration(), pokemon, ); globalScene.ui.setMode( diff --git a/src/data/pokeball.ts b/src/data/pokeball.ts index 50ea5076aee..967928f4d43 100644 --- a/src/data/pokeball.ts +++ b/src/data/pokeball.ts @@ -1,5 +1,6 @@ import { globalScene } from "#app/global-scene"; import { PokeballType } from "#enums/pokeball"; +import { TrainerItemEffect } from "#items/trainer-item"; import { NumberHolder } from "#utils/common"; import i18next from "i18next"; @@ -93,7 +94,9 @@ export function getCriticalCaptureChance(modifiedCatchRate: number): number { } const dexCount = globalScene.gameData.getSpeciesCount(d => !!d.caughtAttr); const catchingCharmMultiplier = new NumberHolder(1); - globalScene.findModifier(m => m.is("CriticalCatchChanceBoosterModifier"))?.apply(catchingCharmMultiplier); + globalScene.applyPlayerItems(TrainerItemEffect.CRITICAL_CATCH_CHANCE_BOOSTER, { + numberHolder: catchingCharmMultiplier, + }); const dexMultiplier = globalScene.gameMode.isDaily || dexCount > 800 ? 2.5 diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index 75734bf085b..043358d5628 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -10,7 +10,6 @@ import { StatusEffect } from "#enums/status-effect"; import type { TimeOfDay } from "#enums/time-of-day"; import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; -import type { PokemonFormChangeItemModifier } from "#modifiers/modifier"; import { type Constructor, coerceArray } from "#utils/common"; import i18next from "i18next"; @@ -77,16 +76,12 @@ export class SpeciesFormChangeItemTrigger extends SpeciesFormChangeTrigger { } canChange(pokemon: Pokemon): boolean { - return !!globalScene.findModifier(r => { - // Assume that if m has the `formChangeItem` property, then it is a PokemonFormChangeItemModifier - const m = r as PokemonFormChangeItemModifier; - return ( - "formChangeItem" in m && - m.pokemonId === pokemon.id && - m.formChangeItem === this.item && - m.active === this.active - ); - }); + const matchItem = pokemon.heldItemManager.formChangeItems[this.item]; + if (!matchItem) { + return false; + } + console.log("CAN CHANGE FORMS:", matchItem.active === this.active); + return matchItem.active === this.active; } } diff --git a/src/data/trainers/fixed-battle-configs.ts b/src/data/trainers/fixed-battle-configs.ts index bb6d591654b..903743b38f5 100644 --- a/src/data/trainers/fixed-battle-configs.ts +++ b/src/data/trainers/fixed-battle-configs.ts @@ -4,8 +4,8 @@ import { globalScene } from "#app/global-scene"; import { randSeedInt } from "#app/utils/common"; import { BattleType } from "#enums/battle-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; -import { ModifierTier } from "#enums/modifier-tier"; import { PlayerGender } from "#enums/player-gender"; +import { RarityTier } from "#enums/reward-tier"; import { TrainerType } from "#enums/trainer-type"; import { TrainerVariant } from "#enums/trainer-variant"; @@ -45,8 +45,8 @@ export const classicFixedBattles: FixedBattleConfigs = { globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, ), ) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], + .setCustomRewards({ + guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.GREAT, RarityTier.GREAT], allowLuckUpgrades: false, }), [ClassicFixedBossWaves.EVIL_GRUNT_1]: new FixedBattleConfig() @@ -77,8 +77,8 @@ export const classicFixedBattles: FixedBattleConfigs = { globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, ), ) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], + .setCustomRewards({ + guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.GREAT, RarityTier.GREAT], allowLuckUpgrades: false, }), [ClassicFixedBossWaves.EVIL_GRUNT_2]: new FixedBattleConfig() @@ -150,8 +150,8 @@ export const classicFixedBattles: FixedBattleConfigs = { globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, ), ) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], + .setCustomRewards({ + guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.ULTRA], allowLuckUpgrades: false, }), [ClassicFixedBossWaves.EVIL_GRUNT_4]: new FixedBattleConfig() @@ -212,14 +212,8 @@ export const classicFixedBattles: FixedBattleConfigs = { TrainerType.PENNY, ]), ) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ], + .setCustomRewards({ + guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.ULTRA], allowLuckUpgrades: false, }), [ClassicFixedBossWaves.RIVAL_5]: new FixedBattleConfig() @@ -231,14 +225,8 @@ export const classicFixedBattles: FixedBattleConfigs = { globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, ), ) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ], + .setCustomRewards({ + guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ULTRA, RarityTier.ULTRA], allowLuckUpgrades: false, }), [ClassicFixedBossWaves.EVIL_BOSS_2]: new FixedBattleConfig() @@ -258,14 +246,14 @@ export const classicFixedBattles: FixedBattleConfigs = { TrainerType.PENNY_2, ]), ) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, + .setCustomRewards({ + guaranteedRarityTiers: [ + RarityTier.ROGUE, + RarityTier.ROGUE, + RarityTier.ULTRA, + RarityTier.ULTRA, + RarityTier.ULTRA, + RarityTier.ULTRA, ], allowLuckUpgrades: false, }), @@ -362,14 +350,14 @@ export const classicFixedBattles: FixedBattleConfigs = { globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, ), ) - .setCustomModifierRewards({ - guaranteedModifierTiers: [ - ModifierTier.ROGUE, - ModifierTier.ROGUE, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.GREAT, - ModifierTier.GREAT, + .setCustomRewards({ + guaranteedRarityTiers: [ + RarityTier.ROGUE, + RarityTier.ROGUE, + RarityTier.ULTRA, + RarityTier.ULTRA, + RarityTier.GREAT, + RarityTier.GREAT, ], allowLuckUpgrades: false, }), diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 5739492f96e..3b30be5c354 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -3,7 +3,7 @@ import { globalScene } from "#app/global-scene"; import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { signatureSpecies } from "#balance/signature-species"; import { tmSpecies } from "#balance/tms"; -import { modifierTypes } from "#data/data-lists"; +import { allRewards } from "#data/data-lists"; import { doubleBattleDialogue } from "#data/double-battle-dialogue"; import { Gender } from "#data/gender"; import type { PokemonSpecies, PokemonSpeciesFilter } from "#data/pokemon-species"; @@ -31,10 +31,10 @@ import { TrainerPartyTemplate, trainerPartyTemplates, } from "#trainers/trainer-party-template"; -import type { ModifierTypeFunc } from "#types/modifier-types"; +import type { RewardFunc } from "#types/rewards"; import type { GenAIFunc, - GenModifiersFunc, + GenTrainerItemsFunc, PartyMemberFunc, PartyMemberFuncs, PartyTemplateFunc, @@ -107,9 +107,9 @@ export class TrainerConfig { public femaleEncounterBgm: string; public doubleEncounterBgm: string; public victoryBgm: string; - public genModifiersFunc: GenModifiersFunc; + public genTrainerItemsFunc: GenTrainerItemsFunc; public genAIFuncs: GenAIFunc[] = []; - public modifierRewardFuncs: ModifierTypeFunc[] = []; + public rewardFuncs: RewardFunc[] = []; public partyTemplates: TrainerPartyTemplate[]; public partyTemplateFunc: PartyTemplateFunc; public partyMemberFuncs: PartyMemberFuncs = {}; @@ -459,8 +459,8 @@ export class TrainerConfig { return this; } - setGenModifiersFunc(genModifiersFunc: GenModifiersFunc): TrainerConfig { - this.genModifiersFunc = genModifiersFunc; + setGenTrainerItemsFunc(genTrainerItemsFunc: GenTrainerItemsFunc): TrainerConfig { + this.genTrainerItemsFunc = genTrainerItemsFunc; return this; } @@ -470,7 +470,7 @@ export class TrainerConfig { * @param slot Optional, a specified slot that should be terastallized. Wraps to match party size (-1 will get the last slot and so on). * @returns this */ - setRandomTeraModifiers(count: () => number, slot?: number): TrainerConfig { + setRandomTeraType(count: () => number, slot?: number): TrainerConfig { this.genAIFuncs.push((party: EnemyPokemon[]) => { const shedinjaCanTera = !this.hasSpecialtyType() || this.specialtyType === PokemonType.BUG; // Better to check one time than 6 const partyMemberIndexes = new Array(party.length) @@ -501,23 +501,11 @@ export class TrainerConfig { return this; } - // function getRandomTeraModifiers(party: EnemyPokemon[], count: integer, types?: Type[]): PersistentModifier[] { - // const ret: PersistentModifier[] = []; - // const partyMemberIndexes = new Array(party.length).fill(null).map((_, i) => i); - // for (let t = 0; t < Math.min(count, party.length); t++) { - // const randomIndex = Utils.randSeedItem(partyMemberIndexes); - // partyMemberIndexes.splice(partyMemberIndexes.indexOf(randomIndex), 1); - // ret.push(modifierTypes.TERA_SHARD().generateType([], [ Utils.randSeedItem(types ? types : party[randomIndex].getTypes()) ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(party[randomIndex]) as PersistentModifier); // TODO: is the bang correct? - // } - // return ret; - // } - - setModifierRewardFuncs(...modifierTypeFuncs: (() => ModifierTypeFunc)[]): TrainerConfig { - this.modifierRewardFuncs = modifierTypeFuncs.map(func => () => { - const modifierTypeFunc = func(); - const modifierType = modifierTypeFunc(); - modifierType.withIdFromFunc(modifierTypeFunc); - return modifierType; + setRewardFuncs(...rewardFuncs: (() => RewardFunc)[]): TrainerConfig { + this.rewardFuncs = rewardFuncs.map(func => () => { + const rewardFunc = func(); + const reward = rewardFunc(); + return reward; }); return this; } @@ -683,7 +671,7 @@ export class TrainerConfig { this.setHasVoucher(true); this.setBattleBgm("battle_unova_gym"); this.setVictoryBgm("victory_gym"); - this.setRandomTeraModifiers( + this.setRandomTeraType( () => (ignoreMinTeraWave || globalScene.currentBattle.waveIndex >= GYM_LEADER_TERA_WAVE ? 1 : 0), teraSlot, ); @@ -744,7 +732,7 @@ export class TrainerConfig { this.setHasVoucher(true); this.setBattleBgm("battle_unova_elite"); this.setVictoryBgm("victory_gym"); - this.setRandomTeraModifiers(() => 1, teraSlot); + this.setRandomTeraType(() => 1, teraSlot); return this; } @@ -921,11 +909,11 @@ export class TrainerConfig { clone = this.battleBgm ? clone.setBattleBgm(this.battleBgm) : clone; clone = this.encounterBgm ? clone.setEncounterBgm(this.encounterBgm) : clone; clone = this.victoryBgm ? clone.setVictoryBgm(this.victoryBgm) : clone; - clone = this.genModifiersFunc ? clone.setGenModifiersFunc(this.genModifiersFunc) : clone; + clone = this.genTrainerItemsFunc ? clone.setGenTrainerItemsFunc(this.genTrainerItemsFunc) : clone; - if (this.modifierRewardFuncs) { + if (this.rewardFuncs) { // Clones array instead of passing ref - clone.modifierRewardFuncs = this.modifierRewardFuncs.slice(0); + clone.rewardFuncs = this.rewardFuncs.slice(0); } if (this.partyTemplates) { @@ -993,6 +981,7 @@ export function getRandomPartyMemberFunc( undefined, false, undefined, + undefined, postProcess, ); }; @@ -1017,7 +1006,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, + ); }; } @@ -4473,9 +4471,9 @@ export const trainerConfigs: TrainerConfigs = { .setBattleBgm("battle_rival") .setMixedBattleBgm("battle_rival") .setPartyTemplates(trainerPartyTemplates.RIVAL) - .setModifierRewardFuncs( - () => modifierTypes.SUPER_EXP_CHARM, - () => modifierTypes.EXP_SHARE, + .setRewardFuncs( + () => allRewards.SUPER_EXP_CHARM, + () => allRewards.EXP_SHARE, ) .setPartyMemberFunc( 0, @@ -4543,7 +4541,7 @@ export const trainerConfigs: TrainerConfigs = { .setBattleBgm("battle_rival") .setMixedBattleBgm("battle_rival") .setPartyTemplates(trainerPartyTemplates.RIVAL_2) - .setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) + .setRewardFuncs(() => allRewards.EXP_SHARE) .setPartyMemberFunc( 0, getRandomPartyMemberFunc( @@ -4696,7 +4694,7 @@ export const trainerConfigs: TrainerConfigs = { .setBattleBgm("battle_rival_2") .setMixedBattleBgm("battle_rival_2") .setPartyTemplates(trainerPartyTemplates.RIVAL_4) - .setModifierRewardFuncs(() => modifierTypes.TERA_ORB) + .setRewardFuncs(() => allRewards.TERA_ORB) .setPartyMemberFunc( 0, getRandomPartyMemberFunc( diff --git a/src/enums/held-item-effect.ts b/src/enums/held-item-effect.ts new file mode 100644 index 00000000000..52aeb13faec --- /dev/null +++ b/src/enums/held-item-effect.ts @@ -0,0 +1,37 @@ +import type { ObjectValues } from "#types/type-helpers"; + +/** + * Enum representing the various "classes" of item effects that can be applied. + */ +export const HeldItemEffect = { + 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 HeldItemEffect = ObjectValues; diff --git a/src/enums/held-item-id.ts b/src/enums/held-item-id.ts new file mode 100644 index 00000000000..91d1901cb4c --- /dev/null +++ b/src/enums/held-item-id.ts @@ -0,0 +1,151 @@ +import type { ObjectValues } from "#types/type-helpers"; + +// TODO: make category the lower 2 bytes +export const HeldItemId = { + NONE: 0x0000, + + // Berries + SITRUS_BERRY: 0x0101, + LUM_BERRY: 0x0102, + ENIGMA_BERRY: 0x0103, + LIECHI_BERRY: 0x0104, + GANLON_BERRY: 0x0105, + PETAYA_BERRY: 0x0106, + APICOT_BERRY: 0x0107, + SALAC_BERRY: 0x0108, + LANSAT_BERRY: 0x0109, + STARF_BERRY: 0x010A, + LEPPA_BERRY: 0x010B, + + // Other items that are consumed + REVIVER_SEED: 0x0201, + WHITE_HERB: 0x0202, + + // Type Boosters + SILK_SCARF: 0x0301, + BLACK_BELT: 0x0302, + SHARP_BEAK: 0x0303, + POISON_BARB: 0x0304, + SOFT_SAND: 0x0305, + HARD_STONE: 0x0306, + SILVER_POWDER: 0x0307, + SPELL_TAG: 0x0308, + METAL_COAT: 0x0309, + CHARCOAL: 0x030A, + MYSTIC_WATER: 0x030B, + MIRACLE_SEED: 0x030C, + MAGNET: 0x030D, + TWISTED_SPOON: 0x030E, + NEVER_MELT_ICE: 0x030F, + DRAGON_FANG: 0x0310, + BLACK_GLASSES: 0x0311, + FAIRY_FEATHER: 0x0312, + + // Species Stat Boosters + LIGHT_BALL: 0x0401, + THICK_CLUB: 0x0402, + METAL_POWDER: 0x0403, + QUICK_POWDER: 0x0404, + DEEP_SEA_SCALE: 0x0405, + DEEP_SEA_TOOTH: 0x0406, + + // Crit Boosters + SCOPE_LENS: 0x0501, + LEEK: 0x0502, + + // Items increasing gains + LUCKY_EGG: 0x0601, + GOLDEN_EGG: 0x0602, + SOOTHE_BELL: 0x0603, + + // Unique items + FOCUS_BAND: 0x0701, + QUICK_CLAW: 0x0702, + KINGS_ROCK: 0x0703, + LEFTOVERS: 0x0704, + SHELL_BELL: 0x0705, + MYSTICAL_ROCK: 0x0706, + WIDE_LENS: 0x0707, + MULTI_LENS: 0x0708, + GOLDEN_PUNCH: 0x0709, + GRIP_CLAW: 0x070A, + TOXIC_ORB: 0x070B, + FLAME_ORB: 0x070C, + SOUL_DEW: 0x070D, + BATON: 0x070E, + MINI_BLACK_HOLE: 0x070F, + EVIOLITE: 0x0710, + + // Vitamins + HP_UP: 0x0801, + PROTEIN: 0x0802, + IRON: 0x0803, + CALCIUM: 0x0804, + ZINC: 0x0805, + CARBOS: 0x0806, + + // Other stat boosting items + SHUCKLE_JUICE_GOOD: 0x0901, + SHUCKLE_JUICE_BAD: 0x0902, + OLD_GATEAU: 0x0903, + MACHO_BRACE: 0x0904, + + // Evo trackers + GIMMIGHOUL_EVO_TRACKER: 0x0A01, +} as const; + +export type HeldItemId = ObjectValues; + +type HeldItemNameMap = { + [k in HeldItemName as (typeof HeldItemId)[k]]: k +} + +type HeldItemName = keyof typeof HeldItemId; + +/** `const object` mapping all held item IDs to their respective names. */ +// TODO: This stores names as UPPER_SNAKE_CASE, but the locales are in PascalCase... +export const HeldItemNames = Object.freeze(Object.entries(HeldItemId).reduce( + // Use a type-safe reducer to force number keys and values + (acc, [key, value]) => { + acc[value] = key; + return acc; + }, + {} +)) as HeldItemNameMap; + +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, +} as const; + +export type HeldItemCategoryId = ObjectValues; + +const ITEM_CATEGORY_MASK = 0xFF00 + +export function getHeldItemCategory(itemId: HeldItemId): HeldItemCategoryId { + return (itemId & ITEM_CATEGORY_MASK) as HeldItemCategoryId; +} + +export function isCategoryId(id: number): id is HeldItemCategoryId { + return (Object.values(HeldItemCategoryId) as number[]).includes(id); +} + +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 deleted file mode 100644 index 0d2b92ba80d..00000000000 --- a/src/enums/modifier-pool-type.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ModifierPoolType { - PLAYER, - WILD, - TRAINER, - ENEMY_BUFF, - DAILY_STARTER -} diff --git a/src/enums/reward-id.ts b/src/enums/reward-id.ts new file mode 100644 index 00000000000..f8179828665 --- /dev/null +++ b/src/enums/reward-id.ts @@ -0,0 +1,100 @@ +import type { ObjectValues } from "#types/type-helpers"; + +export const RewardId = { + NONE: 0x0000, + + POKEBALL: 0x2001, + GREAT_BALL: 0x2002, + ULTRA_BALL: 0x2003, + ROGUE_BALL: 0x2004, + MASTER_BALL: 0x2005, + + VOUCHER: 0x2101, + VOUCHER_PLUS: 0x2102, + VOUCHER_PREMIUM: 0x2103, + + NUGGET: 0x2201, + BIG_NUGGET: 0x2202, + RELIC_GOLD: 0x2203, + + RARE_CANDY: 0x2301, + RARER_CANDY: 0x2302, + + EVOLUTION_ITEM: 0x2401, + RARE_EVOLUTION_ITEM: 0x2402, + + POTION: 0x2501, + SUPER_POTION: 0x2502, + HYPER_POTION: 0x2503, + MAX_POTION: 0x2504, + FULL_HEAL: 0x2505, + FULL_RESTORE: 0x2506, + + REVIVE: 0x2601, + MAX_REVIVE: 0x2602, + SACRED_ASH: 0x2603, + + ETHER: 0x2701, + MAX_ETHER: 0x2702, + + ELIXIR: 0x2801, + MAX_ELIXIR: 0x2802, + + PP_UP: 0x2901, + PP_MAX: 0x2902, + + TM_COMMON: 0x2A01, + TM_GREAT: 0x2A02, + TM_ULTRA: 0x2A03, + + MINT: 0x2B01, + TERA_SHARD: 0x2B02, + MEMORY_MUSHROOM: 0x2B03, + DNA_SPLICERS: 0x2B04, + + HELD_ITEM: 0x2C01, + SPECIES_STAT_BOOSTER: 0x2C02, + RARE_SPECIES_STAT_BOOSTER: 0x2C03, + BASE_STAT_BOOSTER: 0x2C04, + ATTACK_TYPE_BOOSTER: 0x2C05, + BERRY: 0x2C06, + + TRAINER_ITEM: 0x2D01, + TEMP_STAT_STAGE_BOOSTER: 0x2D02, + DIRE_HIT: 0x2D03, + LURE: 0x2D04, + SUPER_LURE: 0x2D05, + MAX_LURE: 0x2D06, + + FORM_CHANGE_ITEM: 0x2E01, + RARE_FORM_CHANGE_ITEM: 0x2E02, +} as const; + +export type RewardId = ObjectValues; + +export const RewardCategoryId = { + NONE: 0x0000, + POKEBALL: 0x0100, + VOUCHER: 0x0200, + MONEY: 0x0300, + CANDY: 0x0400, + EVOLUTION_ITEM: 0x0500, + HEALING: 0x0600, + REVIVE: 0x0700, + ETHER: 0x0800, + ELIXIR: 0x0900, + PP_UP: 0x0A00, + TM: 0x0B00, + OTHER: 0x0C00, + HELD_ITEM: 0x0D00, + TRAINER_ITEM: 0x0E00, + FORM_CHANGE_ITEM: 0x0F00, +} as const; + +export type RewardCategoryId = ObjectValues; + +const ITEM_CATEGORY_MASK = 0xFF00 + +export function getRewardCategory(itemId: RewardId): RewardCategoryId { + return (itemId & ITEM_CATEGORY_MASK) as RewardCategoryId; +} diff --git a/src/enums/reward-pool-type.ts b/src/enums/reward-pool-type.ts new file mode 100644 index 00000000000..fa8c3834009 --- /dev/null +++ b/src/enums/reward-pool-type.ts @@ -0,0 +1,13 @@ +export enum RewardPoolType { + PLAYER, +} + +export enum HeldItemPoolType { + WILD, + TRAINER, + DAILY_STARTER, +} + +export enum TrainerItemPoolType { + ENEMY_BUFF, +} \ No newline at end of file diff --git a/src/enums/modifier-tier.ts b/src/enums/reward-tier.ts similarity index 68% rename from src/enums/modifier-tier.ts rename to src/enums/reward-tier.ts index d8a75e41b0a..51b799a8a5c 100644 --- a/src/enums/modifier-tier.ts +++ b/src/enums/reward-tier.ts @@ -1,4 +1,4 @@ -export enum ModifierTier { +export enum RarityTier { COMMON, GREAT, ULTRA, diff --git a/src/enums/trainer-item-id.ts b/src/enums/trainer-item-id.ts new file mode 100644 index 00000000000..9d07e5c3c50 --- /dev/null +++ b/src/enums/trainer-item-id.ts @@ -0,0 +1,68 @@ +export const TrainerItemId = { + NONE: 0x0000, + +MAP: 0x1001, +IV_SCANNER: 0x1002, +LOCK_CAPSULE: 0x1003, +MEGA_BRACELET: 0x1004, +DYNAMAX_BAND: 0x1005, +TERA_ORB: 0x1006, + +GOLDEN_POKEBALL: 0x1007, + +OVAL_CHARM: 0x1008, +EXP_SHARE: 0x1009, +EXP_BALANCE: 0x100A, + +CANDY_JAR: 0x100B, +BERRY_POUCH: 0x100C, + +HEALING_CHARM: 0x100D, +EXP_CHARM: 0x100E, +SUPER_EXP_CHARM: 0x100F, +GOLDEN_EXP_CHARM: 0x1010, +AMULET_COIN: 0x1011, + +ABILITY_CHARM: 0x1012, +SHINY_CHARM: 0x1013, +CATCHING_CHARM: 0x1014, + +BLACK_SLUDGE: 0x1015, +GOLDEN_BUG_NET: 0x1016, + +LURE: 0x1101, +SUPER_LURE: 0x1102, +MAX_LURE: 0x1103, + +X_ATTACK: 0x1201, +X_DEFENSE: 0x1202, +X_SP_ATK: 0x1203, +X_SP_DEF: 0x1204, +X_SPEED: 0x1205, +X_ACCURACY: 0x1206, +DIRE_HIT: 0x1207, + +ENEMY_DAMAGE_BOOSTER: 0x1301, +ENEMY_DAMAGE_REDUCTION: 0x1302, +ENEMY_HEAL: 0x1303, +ENEMY_ATTACK_POISON_CHANCE: 0x1304, +ENEMY_ATTACK_PARALYZE_CHANCE: 0x1305, +ENEMY_ATTACK_BURN_CHANCE: 0x1306, +ENEMY_STATUS_EFFECT_HEAL_CHANCE: 0x1307, +ENEMY_ENDURE_CHANCE: 0x1308, +ENEMY_FUSED_CHANCE: 0x1309, +} as const; + +export type TrainerItemId = (typeof TrainerItemId)[keyof typeof TrainerItemId]; + +type TrainerItemName = keyof typeof TrainerItemId; +type TrainerItemValue = typeof TrainerItemId[TrainerItemName]; + +// Use a type-safe reducer to force number keys and values +export const TrainerItemNames: Record = Object.entries(TrainerItemId).reduce( + (acc, [key, value]) => { + acc[value as TrainerItemValue] = key as TrainerItemName; + return acc; + }, + {} as Record +); \ No newline at end of file diff --git a/src/enums/ui-mode.ts b/src/enums/ui-mode.ts index dcf6bd2a238..ffe742b9d8e 100644 --- a/src/enums/ui-mode.ts +++ b/src/enums/ui-mode.ts @@ -5,7 +5,7 @@ export enum UiMode { FIGHT, BALL, TARGET_SELECT, - MODIFIER_SELECT, + REWARD_SELECT, SAVE_SLOT, PARTY, SUMMARY, diff --git a/src/events/battle-scene.ts b/src/events/battle-scene.ts index 29aee1053cd..469820946ad 100644 --- a/src/events/battle-scene.ts +++ b/src/events/battle-scene.ts @@ -1,4 +1,5 @@ -import type { BerryModifier } from "#modifiers/modifier"; +import type { BerryType } from "#enums/berry-type"; +import type { Pokemon } from "#field/pokemon"; import type { Move } from "#moves/move"; /** Alias for all {@linkcode BattleScene} events */ @@ -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 484450cc5df..adf62facc23 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -24,6 +24,7 @@ import { ArenaTagSide } from "#enums/arena-tag-side"; import type { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerIndex } from "#enums/battler-index"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemEffect } from "#enums/held-item-effect"; import { CommonAnim } from "#enums/move-anims-common"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; @@ -33,7 +34,7 @@ import { TrainerType } from "#enums/trainer-type"; import { WeatherType } from "#enums/weather-type"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#events/arena"; import type { Pokemon } from "#field/pokemon"; -import { FieldEffectModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import type { Move } from "#moves/move"; import type { AbstractConstructor } from "#types/type-helpers"; import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common"; @@ -341,7 +342,7 @@ export class Arena { if (!isNullOrUndefined(user)) { weatherDuration.value = 5; - globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); + applyHeldItems(HeldItemEffect.FIELD_EFFECT, { pokemon: user, fieldDuration: weatherDuration }); } this.weather = weather ? new Weather(weather, weatherDuration.value) : null; @@ -428,7 +429,7 @@ export class Arena { if (!isNullOrUndefined(user)) { terrainDuration.value = 5; - globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); + applyHeldItems(HeldItemEffect.FIELD_EFFECT, { pokemon: user, fieldDuration: terrainDuration }); } this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null; diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index d3776f349e6..1ebff9d7515 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -336,7 +336,7 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { tryPlaySprite( sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, - animConfig: Phaser.Types.Animations.PlayAnimationConfig, + animConfig: PlayAnimationConfig, ): boolean { // Show an error in the console if there isn't a texture loaded if (sprite.texture.key === "__MISSING") { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0523671ee5f..64594084060 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -79,9 +79,10 @@ import { ChallengeType } from "#enums/challenge-type"; import { Challenges } from "#enums/challenges"; import { DexAttr } from "#enums/dex-attr"; import { FieldPosition } from "#enums/field-position"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; import { HitResult } from "#enums/hit-result"; import { LearnMoveSituation } from "#enums/learn-move-situation"; -import { ModifierTier } from "#enums/modifier-tier"; import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; @@ -91,6 +92,7 @@ import { Nature } from "#enums/nature"; import { PokeballType } from "#enums/pokeball"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonType } from "#enums/pokemon-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesId } from "#enums/species-id"; import { @@ -108,27 +110,11 @@ import type { TrainerSlot } from "#enums/trainer-slot"; import { UiMode } from "#enums/ui-mode"; import { WeatherType } from "#enums/weather-type"; import { doShinySparkleAnim } from "#field/anims"; -import { - BaseStatModifier, - CritBoosterModifier, - EnemyDamageBoosterModifier, - EnemyDamageReducerModifier, - EnemyFusionChanceModifier, - EvoTrackerModifier, - HiddenAbilityRateBoosterModifier, - PokemonBaseStatFlatModifier, - PokemonBaseStatTotalModifier, - PokemonFriendshipBoosterModifier, - PokemonHeldItemModifier, - PokemonIncrementingStatModifier, - PokemonMultiHitModifier, - PokemonNatureWeightModifier, - ShinyRateBoosterModifier, - StatBoosterModifier, - SurviveDamageModifier, - TempCritBoosterModifier, - TempStatStageBoosterModifier, -} from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; +import type { HeldItemConfiguration } from "#items/held-item-data-types"; +import { HeldItemManager } from "#items/held-item-manager"; +import { assignItemsFromConfiguration } from "#items/held-item-pool"; +import { TrainerItemEffect } from "#items/trainer-item"; import { applyMoveAttrs } from "#moves/apply-attrs"; import type { Move } from "#moves/move"; import { getMoveTargets } from "#moves/move-utils"; @@ -289,6 +275,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { private shinySparkle: Phaser.GameObjects.Sprite; + public heldItemManager: HeldItemManager; + // TODO: Rework this eventually constructor( x: number, @@ -302,6 +290,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { variant?: Variant, ivs?: number[], nature?: Nature, + heldItemConfig?: HeldItemConfiguration, dataSource?: Pokemon | PokemonData, ) { super(globalScene, x, y); @@ -331,6 +320,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate); this.levelExp = dataSource?.levelExp || 0; + this.heldItemManager = new HeldItemManager(); + if (heldItemConfig) { + assignItemsFromConfiguration(heldItemConfig, this); + } + if (dataSource) { this.id = dataSource.id; this.hp = dataSource.hp; @@ -408,7 +402,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (level > 1) { const fused = new BooleanHolder(globalScene.gameMode.isSplicedOnly); if (!fused.value && this.isEnemy() && !this.hasTrainer()) { - globalScene.applyModifier(EnemyFusionChanceModifier, false, fused); + globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_FUSED_CHANCE, { booleanHolder: fused }); } if (fused.value) { @@ -596,7 +590,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { // Roll for hidden ability chance, applying any ability charms for enemy mons const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); } // If the roll succeeded and we have one, use HA; otherwise pick a random ability @@ -1152,14 +1148,8 @@ export 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 { @@ -1383,8 +1373,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { getCritStage(source: Pokemon, move: Move): number { const critStage = new NumberHolder(0); applyMoveAttrs("HighCritAttr", source, this, move, critStage); - globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage); - globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); + applyHeldItems(HeldItemEffect.CRIT_BOOST, { pokemon: source, critStage: critStage }); + globalScene.applyPlayerItems(TrainerItemEffect.TEMP_CRIT_BOOSTER, { numberHolder: critStage }); applyAbAttrs("BonusCritAbAttr", { pokemon: source, critStage }); const critBoostTag = source.getTag(CritBoostTag); if (critBoostTag) { @@ -1439,7 +1429,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ): number { const statVal = new NumberHolder(this.getStat(stat, false)); if (!ignoreHeldItems) { - globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statVal); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: this, stat: stat, statValue: statVal }); } // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway @@ -1549,7 +1539,7 @@ export 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(HeldItemEffect.INCREMENTING_STAT, { pokemon: this, stat: s, statHolder: statHolder }); if (this.hasAbility(AbilityId.WONDER_GUARD, false, true)) { statHolder.value = 1; } @@ -1564,14 +1554,14 @@ export 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(HeldItemEffect.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(HeldItemEffect.INCREMENTING_STAT, { pokemon: this, stat: s, statHolder: statHolder }); } statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, Number.MAX_SAFE_INTEGER); @@ -1584,9 +1574,9 @@ export 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(HeldItemEffect.BASE_STAT_TOTAL, { pokemon: this, baseStats: baseStats }); // Old Gateau - globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); + applyHeldItems(HeldItemEffect.BASE_STAT_FLAT, { pokemon: this, baseStats: baseStats }); if (this.isFusion()) { const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; applyChallenges(ChallengeType.FLIP_STAT, this, fusionBaseStats); @@ -1600,7 +1590,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } // Vitamins - globalScene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); + applyHeldItems(HeldItemEffect.BASE_STAT_BOOSTER, { pokemon: this, baseStats: baseStats }); return baseStats; } @@ -2231,8 +2221,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first - * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) + * Gets the weight of the Pokemon with subtractive abilities (Autotomize) happening first + * and then multiplicative abilities happening after (Heavy Metal and Light Metal) * @returns the kg of the Pokemon (minimum of 0.1) */ public getWeight(): number { @@ -2857,7 +2847,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } if (!this.hasTrainer()) { - globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + globalScene.applyPlayerItems(TrainerItemEffect.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold }); } } else { shinyThreshold.value = thresholdOverride; @@ -2879,17 +2869,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} + * @param applyItemsToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise */ - public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { + public trySetShinySeed(thresholdOverride?: number, applyItemsToOverride?: boolean): boolean { if (!this.shiny) { const shinyThreshold = new NumberHolder(thresholdOverride ?? BASE_SHINY_CHANCE); - if (applyModifiersToOverride) { + if (applyItemsToOverride) { if (timedEventManager.isEventActive()) { shinyThreshold.value *= timedEventManager.getShinyMultiplier(); } - globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + globalScene.applyPlayerItems(TrainerItemEffect.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold }); } this.shiny = randSeedInt(65536) < shinyThreshold.value; @@ -2951,17 +2941,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * The base hidden ability odds are {@linkcode BASE_HIDDEN_ABILITY_CHANCE} / `65536` * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} + * @param applyItemsToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} * @returns `true` if the Pokemon has been set to have its hidden ability, `false` otherwise */ - public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { + public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyItemsToOverride?: boolean): boolean { if (!this.species.abilityHidden) { return false; } const haThreshold = new NumberHolder(thresholdOverride ?? BASE_HIDDEN_ABILITY_CHANCE); - if (applyModifiersToOverride) { + if (applyItemsToOverride) { if (!this.hasTrainer()) { - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, haThreshold); + globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, { numberHolder: haThreshold }); } } @@ -2975,7 +2965,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { public generateFusionSpecies(forStarter?: boolean): void { const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { - globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, { + numberHolder: hiddenAbilityChance, + }); } const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); @@ -3103,11 +3095,11 @@ export 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] === RarityTier.COMMON && this.level >= 15) { movePool.push([moveId, 4]); - } else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) { + } else if (tmPoolTiers[moveId] === RarityTier.GREAT && this.level >= 30) { movePool.push([moveId, 8]); - } else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) { + } else if (tmPoolTiers[moveId] === RarityTier.ULTRA && this.level >= 50) { movePool.push([moveId, 14]); } } @@ -3485,7 +3477,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (!ignoreStatStage.value) { const statStageMultiplier = new NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value)); if (!ignoreHeldItems) { - globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); + globalScene.applyPlayerItems(TrainerItemEffect.TEMP_STAT_STAGE_BOOSTER, { + numberHolder: statStageMultiplier, + }); } return Math.min(statStageMultiplier.value, 4); } @@ -3519,7 +3513,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: this, stat: Stat.EVA, ignored: ignoreEvaStatStage }); applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage); - globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); + globalScene.applyPlayerItems(TrainerItemEffect.TEMP_ACCURACY_BOOSTER, { numberHolder: userAccStage }); userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6); targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value; @@ -3772,14 +3766,11 @@ export 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(HeldItemEffect.MULTI_HIT, { + pokemon: source, + moveId: move.id, + damageMultiplier: multiLensMultiplier, + }); fixedDamage.value = toDmgValue(fixedDamage.value * multiLensMultiplier.value); return { @@ -3823,14 +3814,11 @@ export 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(HeldItemEffect.MULTI_HIT, { + pokemon: source, + moveId: move.id, + damageMultiplier: multiStrikeEnhancementMultiplier, + }); if (!ignoreSourceAbility) { applyAbAttrs("AddSecondStrikeAbAttr", { @@ -3949,10 +3937,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** Apply the enemy's Damage and Resistance tokens */ if (!source.isPlayer()) { - globalScene.applyModifiers(EnemyDamageBoosterModifier, false, damage); + globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_DAMAGE_BOOSTER, { numberHolder: damage }); } if (!this.isPlayer()) { - globalScene.applyModifiers(EnemyDamageReducerModifier, false, damage); + globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_DAMAGE_REDUCER, { numberHolder: damage }); } const abAttrParams: PreAttackModifyDamageAbAttrParams = { @@ -3962,7 +3950,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { simulated, damage, }; - /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ + /** Apply this Pokemon's post-calc defensive attributes (e.g. Fur Coat) */ if (!ignoreAbility) { applyAbAttrs("ReceivedMoveDamageMultiplierAbAttr", abAttrParams); @@ -4064,7 +4052,7 @@ export 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(HeldItemEffect.SURVIVE_CHANCE, { pokemon: this, surviveDamage: surviveDamage }); } if (surviveDamage.value) { damage = this.hp - 1; @@ -4513,7 +4501,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.setScale(this.getSpriteScale()); this.loadAssets().then(() => { this.calculateStats(); - globalScene.updateModifiers(this.isPlayer(), true); + globalScene.updateItems(this.isPlayer()); Promise.all([this.updateInfo(), globalScene.updateFieldScale()]).then(() => resolve()); }); }); @@ -5622,16 +5610,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Should be `false` for all item loss occurring outside of battle (MEs, etc.). * @returns Whether the item was removed successfully. */ - public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle = true): boolean { + public loseHeldItem(heldItemId: HeldItemId, forBattle = true): boolean { // TODO: What does a -1 pokemon id mean? - if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) { + if (!this.heldItemManager.hasItem(heldItemId)) { return false; } - heldItem.stackCount--; - if (heldItem.stackCount <= 0) { - globalScene.removeModifier(heldItem, this.isEnemy()); - } + this.heldItemManager.remove(heldItemId); + if (forBattle) { applyAbAttrs("PostItemLostAbAttr", { pokemon: this }); } @@ -5653,13 +5639,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } this.turnData.berriesEaten.push(berryType); } - - getPersistentTreasureCount(): number { - return ( - this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length + - globalScene.findModifiers(m => m.is("MoneyMultiplierModifier") || m.is("ExtraModifierModifier")).length - ); - } } export class PlayerPokemon extends Pokemon { @@ -5676,9 +5655,24 @@ export class PlayerPokemon extends Pokemon { variant?: Variant, ivs?: number[], nature?: Nature, + heldItemConfig?: HeldItemConfiguration, dataSource?: Pokemon | PokemonData, ) { - super(106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource); + super( + 106, + 148, + species, + level, + abilityIndex, + formIndex, + gender, + shiny, + variant, + ivs, + nature, + heldItemConfig, + dataSource, + ); if (Overrides.STATUS_OVERRIDE) { this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4); @@ -5822,7 +5816,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(HeldItemEffect.FRIENDSHIP_BOOSTER, { pokemon: this, friendship: amount }); const candyFriendshipMultiplier = globalScene.gameMode.isClassic ? timedEventManager.getClassicFriendshipMultiplier() : 1; @@ -5882,6 +5876,7 @@ export class PlayerPokemon extends Pokemon { this.variant, this.ivs, this.nature, + this.heldItemManager.generateHeldItemConfiguration(), this, ); this.fusionSpecies = originalFusionSpecies; @@ -5904,6 +5899,7 @@ export class PlayerPokemon extends Pokemon { this.variant, this.ivs, this.nature, + this.heldItemManager.generateHeldItemConfiguration(), this, ); } @@ -5976,9 +5972,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) { @@ -6031,16 +6027,12 @@ export class PlayerPokemon extends Pokemon { globalScene.getPlayerParty().push(newPokemon); newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies); - const modifiers = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, - true, - ) as PokemonHeldItemModifier[]; - modifiers.forEach(m => { - const clonedModifier = m.clone() as PokemonHeldItemModifier; - clonedModifier.pokemonId = newPokemon.id; - globalScene.addModifier(clonedModifier, true); + //TODO: This currently does not consider any values associated with the items e.g. disabled + const heldItems = this.getHeldItems(); + heldItems.forEach(item => { + newPokemon.heldItemManager.add(item, this.heldItemManager.getStack(item)); }); - globalScene.updateModifiers(true); + globalScene.updateItems(true); } } } @@ -6061,6 +6053,7 @@ export class PlayerPokemon extends Pokemon { this.variant, this.ivs, this.nature, + this.heldItemManager.generateHeldItemConfiguration(), this, ); ret.loadAssets().then(() => resolve(ret)); @@ -6085,7 +6078,7 @@ export class PlayerPokemon extends Pokemon { const updateAndResolve = () => { this.loadAssets().then(() => { this.calculateStats(); - globalScene.updateModifiers(true, true); + globalScene.updateItems(true); this.updateInfo(true).then(() => resolve()); }); }; @@ -6151,15 +6144,11 @@ export class PlayerPokemon extends Pokemon { } // combine the two mons' held items - const fusedPartyMemberHeldModifiers = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, - true, - ) as PokemonHeldItemModifier[]; - for (const modifier of fusedPartyMemberHeldModifiers) { - globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false); + const fusedPartyMemberHeldItems = pokemon.getHeldItems(); + for (const item of fusedPartyMemberHeldItems) { + globalScene.tryTransferHeldItem(item, pokemon, this, false, pokemon.heldItemManager.getStack(item), true, false); } - globalScene.updateModifiers(true, true); - globalScene.removePartyMemberModifiers(fusedPartyMemberIndex); + globalScene.updateItems(true); globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this); pokemon @@ -6207,6 +6196,7 @@ export class EnemyPokemon extends Pokemon { trainerSlot: TrainerSlot, boss: boolean, shinyLock = false, + heldItemConfig?: HeldItemConfiguration, dataSource?: PokemonData, ) { super( @@ -6221,6 +6211,7 @@ export class EnemyPokemon extends Pokemon { !shinyLock && dataSource ? dataSource.variant : undefined, undefined, dataSource ? dataSource.nature : undefined, + heldItemConfig, dataSource, ); @@ -6858,6 +6849,7 @@ export class EnemyPokemon extends Pokemon { this.variant, this.ivs, this.nature, + this.heldItemManager.generateHeldItemConfiguration(), this, ); diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 584c9310932..1a284f268cc 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -12,7 +12,7 @@ import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerType } from "#enums/trainer-type"; import { TrainerVariant } from "#enums/trainer-variant"; import type { EnemyPokemon } from "#field/pokemon"; -import type { PersistentModifier } from "#modifiers/modifier"; +import type { TrainerItemConfiguration } from "#items/trainer-item-data-types"; import { getIsInitialized, initI18n } from "#plugins/i18n"; import type { TrainerConfig } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config"; @@ -634,7 +634,7 @@ export class Trainer extends Phaser.GameObjects.Container { return maxScorePartyMemberIndexes[0]; } - getPartyMemberModifierChanceMultiplier(index: number): number { + getPartyMemberItemChanceMultiplier(index: number): number { switch (this.getPartyTemplate().getStrength(index)) { case PartyMemberStrength.WEAKER: return 0.75; @@ -647,14 +647,14 @@ export class Trainer extends Phaser.GameObjects.Container { case PartyMemberStrength.STRONGER: return 0.375; default: - console.warn("getPartyMemberModifierChanceMultiplier not defined. Using default 0"); + console.warn("getPartyMemberItemChanceMultiplier not defined. Using default 0"); return 0; } } - genModifiers(party: EnemyPokemon[]): PersistentModifier[] { - if (this.config.genModifiersFunc) { - return this.config.genModifiersFunc(party); + genTrainerItems(party: EnemyPokemon[]): TrainerItemConfiguration { + if (this.config.genTrainerItemsFunc) { + return this.config.genTrainerItemsFunc(party); } return []; } diff --git a/src/game-mode.ts b/src/game-mode.ts index c5ab120e218..d12e801b196 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -323,7 +323,7 @@ export class GameMode implements GameModeConfig { } } - getEnemyModifierChance(isBoss: boolean): number { + getEnemyItemChance(isBoss: boolean): number { switch (this.modeId) { case GameModes.CLASSIC: case GameModes.CHALLENGE: diff --git a/src/init/init.ts b/src/init/init.ts index ba9738e2be8..4f6731213da 100644 --- a/src/init/init.ts +++ b/src/init/init.ts @@ -6,8 +6,12 @@ import { initSpecies } from "#balance/pokemon-species"; import { initChallenges } from "#data/challenge"; import { initTrainerTypeDialogue } from "#data/dialogue"; import { initPokemonForms } from "#data/pokemon-forms"; -import { initModifierPools } from "#modifiers/init-modifier-pools"; -import { initModifierTypes } from "#modifiers/modifier-type"; +import { initHeldItems } from "#items/all-held-items"; +import { initRewards } from "#items/all-rewards"; +import { initTrainerItems } from "#items/all-trainer-items"; +import { initHeldItemPools } from "#items/init-held-item-pools"; +import { initRewardPools } from "#items/init-reward-pools"; +import { initTrainerItemPools } from "#items/init-trainer-item-pools"; import { initMoves } from "#moves/move"; import { initMysteryEncounters } from "#mystery-encounters/mystery-encounters"; import { initAchievements } from "#system/achv"; @@ -16,10 +20,9 @@ import { initStatsKeys } from "#ui/game-stats-ui-handler"; /** Initialize the game. */ export function initializeGame() { - initModifierTypes(); - initModifierPools(); - initAchievements(); + initItems(); initVouchers(); + initAchievements(); initStatsKeys(); initPokemonPrevolutions(); initPokemonStarters(); @@ -33,3 +36,15 @@ export function initializeGame() { initChallenges(); initMysteryEncounters(); } + +/** + * Sub-method to initialize all the item-related code. + */ +function initItems() { + initHeldItems(); + initHeldItemPools(); + initTrainerItems(); + initTrainerItemPools(); + initRewards(); + initRewardPools(); +} diff --git a/src/items/all-held-items.ts b/src/items/all-held-items.ts new file mode 100644 index 00000000000..d505a28bb52 --- /dev/null +++ b/src/items/all-held-items.ts @@ -0,0 +1,194 @@ +import { allHeldItems } from "#data/data-lists"; +import { BerryType } from "#enums/berry-type"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; +import type { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { type PermanentStat, Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { AccuracyBoosterHeldItem, type AccuracyBoostParams } from "#items/accuracy-booster"; +import { + AttackTypeBoosterHeldItem, + type AttackTypeBoostParams, + attackTypeToHeldItem, +} from "#items/attack-type-booster"; +import { BaseStatBoosterHeldItem, type BaseStatBoosterParams, permanentStatToHeldItem } from "#items/base-stat-booster"; +import { BaseStatFlatHeldItem, type BaseStatFlatParams } from "#items/base-stat-flat"; +import { BaseStatTotalHeldItem, type BaseStatTotalParams } from "#items/base-stat-total"; +import { BatonHeldItem, type BatonParams } from "#items/baton"; +import { BerryHeldItem, type BerryParams, berryTypeToHeldItem } from "#items/berry"; +import { BypassSpeedChanceHeldItem, type BypassSpeedChanceParams } from "#items/bypass-speed-chance"; +import { CritBoostHeldItem, type CritBoostParams, SpeciesCritBoostHeldItem } from "#items/crit-booster"; +import { DamageMoneyRewardHeldItem, type DamageMoneyRewardParams } from "#items/damage-money-reward"; +import { type EvoTrackerParams, GimmighoulEvoTrackerHeldItem } from "#items/evo-tracker"; +import { ExpBoosterHeldItem, type ExpBoostParams } from "#items/exp-booster"; +import { FieldEffectHeldItem, type FieldEffectParams } from "#items/field-effect"; +import { FlinchChanceHeldItem, type FlinchChanceParams } from "#items/flinch-chance"; +import { FriendshipBoosterHeldItem, type FriendshipBoostParams } from "#items/friendship-booster"; +import { HitHealHeldItem, type HitHealParams } from "#items/hit-heal"; +import { IncrementingStatHeldItem, type IncrementingStatParams } from "#items/incrementing-stat"; +import { InstantReviveHeldItem, type InstantReviveParams } from "#items/instant-revive"; +import { ContactItemStealChanceHeldItem, type ItemStealParams, TurnEndItemStealHeldItem } from "#items/item-steal"; +import { MultiHitHeldItem, type MultiHitParams } from "#items/multi-hit"; +import { NatureWeightBoosterHeldItem, type NatureWeightBoostParams } from "#items/nature-weight-booster"; +import { ResetNegativeStatStageHeldItem, type ResetNegativeStatStageParams } from "#items/reset-negative-stat-stage"; +import { EvolutionStatBoostHeldItem, SpeciesStatBoostHeldItem, type StatBoostParams } from "#items/stat-booster"; +import { SurviveChanceHeldItem, type SurviveChanceParams } from "#items/survive-chance"; +import { TurnEndHealHeldItem, type TurnEndHealParams } from "#items/turn-end-heal"; +import { TurnEndStatusHeldItem, type TurnEndStatusParams } from "#items/turn-end-status"; +import { getEnumValues } from "#utils/enums"; + +export function initHeldItems() { + for (const berry of getEnumValues(BerryType)) { + const maxStackCount = [BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(berry) ? 2 : 3; + const berryId = berryTypeToHeldItem[berry]; + allHeldItems[berryId] = new BerryHeldItem(berry, maxStackCount); + } + + allHeldItems[HeldItemId.REVIVER_SEED] = new InstantReviveHeldItem(HeldItemId.REVIVER_SEED, 1); + allHeldItems[HeldItemId.WHITE_HERB] = new ResetNegativeStatStageHeldItem(HeldItemId.WHITE_HERB, 2); + + // SILK_SCARF, BLACK_BELT, etc... + for (const [typeKey, heldItemType] of Object.entries(attackTypeToHeldItem)) { + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114957526 + const pokemonType = Number(typeKey) as PokemonType; + allHeldItems[heldItemType] = new AttackTypeBoosterHeldItem(heldItemType, 99, pokemonType, 0.2); + } + + // Items that boost specific stats + allHeldItems[HeldItemId.EVIOLITE] = new EvolutionStatBoostHeldItem( + HeldItemId.EVIOLITE, + 1, + [Stat.DEF, Stat.SPDEF], + 1.5, + ); + allHeldItems[HeldItemId.LIGHT_BALL] = new SpeciesStatBoostHeldItem( + HeldItemId.LIGHT_BALL, + 1, + [Stat.ATK, Stat.SPATK], + 2, + [SpeciesId.PIKACHU], + ); + allHeldItems[HeldItemId.THICK_CLUB] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.ATK], 2, [ + SpeciesId.CUBONE, + SpeciesId.MAROWAK, + SpeciesId.ALOLA_MAROWAK, + ]); + allHeldItems[HeldItemId.METAL_POWDER] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.DEF], 2, [ + SpeciesId.DITTO, + ]); + allHeldItems[HeldItemId.QUICK_POWDER] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPD], 2, [ + SpeciesId.DITTO, + ]); + allHeldItems[HeldItemId.DEEP_SEA_SCALE] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPDEF], 2, [ + SpeciesId.CLAMPERL, + ]); + allHeldItems[HeldItemId.DEEP_SEA_TOOTH] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPATK], 2, [ + SpeciesId.CLAMPERL, + ]); + + // Items that boost the crit rate + allHeldItems[HeldItemId.SCOPE_LENS] = new CritBoostHeldItem(HeldItemId.SCOPE_LENS, 1, 1); + allHeldItems[HeldItemId.LEEK] = new SpeciesCritBoostHeldItem(HeldItemId.LEEK, 1, 2, [ + SpeciesId.FARFETCHD, + SpeciesId.GALAR_FARFETCHD, + SpeciesId.SIRFETCHD, + ]); + + allHeldItems[HeldItemId.LUCKY_EGG] = new ExpBoosterHeldItem(HeldItemId.LUCKY_EGG, 99, 40); + allHeldItems[HeldItemId.GOLDEN_EGG] = new ExpBoosterHeldItem(HeldItemId.GOLDEN_EGG, 99, 100); + allHeldItems[HeldItemId.SOOTHE_BELL] = new FriendshipBoosterHeldItem(HeldItemId.SOOTHE_BELL, 3); + + allHeldItems[HeldItemId.LEFTOVERS] = new TurnEndHealHeldItem(HeldItemId.LEFTOVERS, 4); + allHeldItems[HeldItemId.SHELL_BELL] = new HitHealHeldItem(HeldItemId.SHELL_BELL, 4); + + allHeldItems[HeldItemId.FOCUS_BAND] = new SurviveChanceHeldItem(HeldItemId.FOCUS_BAND, 5); + allHeldItems[HeldItemId.QUICK_CLAW] = new BypassSpeedChanceHeldItem(HeldItemId.QUICK_CLAW, 3); + allHeldItems[HeldItemId.KINGS_ROCK] = new FlinchChanceHeldItem(HeldItemId.KINGS_ROCK, 3, 10); + allHeldItems[HeldItemId.MYSTICAL_ROCK] = new FieldEffectHeldItem(HeldItemId.MYSTICAL_ROCK, 2); + allHeldItems[HeldItemId.SOUL_DEW] = new NatureWeightBoosterHeldItem(HeldItemId.SOUL_DEW, 10); + allHeldItems[HeldItemId.WIDE_LENS] = new AccuracyBoosterHeldItem(HeldItemId.WIDE_LENS, 3, 5); + allHeldItems[HeldItemId.MULTI_LENS] = new MultiHitHeldItem(HeldItemId.MULTI_LENS, 2); + allHeldItems[HeldItemId.GOLDEN_PUNCH] = new DamageMoneyRewardHeldItem(HeldItemId.GOLDEN_PUNCH, 5); + allHeldItems[HeldItemId.BATON] = new BatonHeldItem(HeldItemId.BATON, 1); + allHeldItems[HeldItemId.GRIP_CLAW] = new ContactItemStealChanceHeldItem(HeldItemId.GRIP_CLAW, 5, 10); + allHeldItems[HeldItemId.MINI_BLACK_HOLE] = new TurnEndItemStealHeldItem(HeldItemId.MINI_BLACK_HOLE, 1) + .unstealable() + .untransferable(); + + allHeldItems[HeldItemId.FLAME_ORB] = new TurnEndStatusHeldItem(HeldItemId.FLAME_ORB, 1, StatusEffect.BURN); + allHeldItems[HeldItemId.TOXIC_ORB] = new TurnEndStatusHeldItem(HeldItemId.TOXIC_ORB, 1, StatusEffect.TOXIC); + + // vitamins + for (const [statKey, heldItemType] of Object.entries(permanentStatToHeldItem)) { + const stat = Number(statKey) as PermanentStat; + allHeldItems[heldItemType] = new BaseStatBoosterHeldItem(heldItemType, 30, stat) + .unstealable() + .untransferable() + .unsuppressable(); + } + + allHeldItems[HeldItemId.SHUCKLE_JUICE_GOOD] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE_GOOD, 1, 10) + .unstealable() + .untransferable() + .unsuppressable(); + allHeldItems[HeldItemId.SHUCKLE_JUICE_BAD] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE_BAD, 1, -15) + .unstealable() + .untransferable() + .unsuppressable(); + allHeldItems[HeldItemId.OLD_GATEAU] = new BaseStatFlatHeldItem(HeldItemId.OLD_GATEAU, 1) + .unstealable() + .untransferable() + .unsuppressable(); + allHeldItems[HeldItemId.MACHO_BRACE] = new IncrementingStatHeldItem(HeldItemId.MACHO_BRACE, 50) + .unstealable() + .untransferable() + .unsuppressable(); + allHeldItems[HeldItemId.GIMMIGHOUL_EVO_TRACKER] = new GimmighoulEvoTrackerHeldItem( + HeldItemId.GIMMIGHOUL_EVO_TRACKER, + 999, + SpeciesId.GIMMIGHOUL, + 10, + ); +} + +type ApplyHeldItemsParams = { + [HeldItemEffect.ATTACK_TYPE_BOOST]: AttackTypeBoostParams; + [HeldItemEffect.TURN_END_HEAL]: TurnEndHealParams; + [HeldItemEffect.HIT_HEAL]: HitHealParams; + [HeldItemEffect.RESET_NEGATIVE_STAT_STAGE]: ResetNegativeStatStageParams; + [HeldItemEffect.EXP_BOOSTER]: ExpBoostParams; + [HeldItemEffect.BERRY]: BerryParams; + [HeldItemEffect.BASE_STAT_BOOSTER]: BaseStatBoosterParams; + [HeldItemEffect.INSTANT_REVIVE]: InstantReviveParams; + [HeldItemEffect.STAT_BOOST]: StatBoostParams; + [HeldItemEffect.CRIT_BOOST]: CritBoostParams; + [HeldItemEffect.TURN_END_STATUS]: TurnEndStatusParams; + [HeldItemEffect.SURVIVE_CHANCE]: SurviveChanceParams; + [HeldItemEffect.BYPASS_SPEED_CHANCE]: BypassSpeedChanceParams; + [HeldItemEffect.FLINCH_CHANCE]: FlinchChanceParams; + [HeldItemEffect.FIELD_EFFECT]: FieldEffectParams; + [HeldItemEffect.FRIENDSHIP_BOOSTER]: FriendshipBoostParams; + [HeldItemEffect.NATURE_WEIGHT_BOOSTER]: NatureWeightBoostParams; + [HeldItemEffect.ACCURACY_BOOSTER]: AccuracyBoostParams; + [HeldItemEffect.MULTI_HIT]: MultiHitParams; + [HeldItemEffect.DAMAGE_MONEY_REWARD]: DamageMoneyRewardParams; + [HeldItemEffect.BATON]: BatonParams; + [HeldItemEffect.CONTACT_ITEM_STEAL_CHANCE]: ItemStealParams; + [HeldItemEffect.TURN_END_ITEM_STEAL]: ItemStealParams; + [HeldItemEffect.BASE_STAT_TOTAL]: BaseStatTotalParams; + [HeldItemEffect.BASE_STAT_FLAT]: BaseStatFlatParams; + [HeldItemEffect.INCREMENTING_STAT]: IncrementingStatParams; + [HeldItemEffect.EVO_TRACKER]: EvoTrackerParams; +}; + +export function applyHeldItems(effect: T, params: ApplyHeldItemsParams[T]) { + const pokemon = params.pokemon; + if (pokemon) { + for (const item of Object.keys(pokemon.heldItemManager.heldItems)) { + if (allHeldItems[item].effects.includes(effect)) { + allHeldItems[item].apply(params); + } + } + } +} diff --git a/src/items/all-rewards.ts b/src/items/all-rewards.ts new file mode 100644 index 00000000000..4ab8def39a1 --- /dev/null +++ b/src/items/all-rewards.ts @@ -0,0 +1,182 @@ +import { allRewards } from "#data/data-lists"; +import { PokeballType } from "#enums/pokeball"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { VoucherType } from "#system/voucher"; +import { + AddMoneyReward, + AddPokeballReward, + AddVoucherReward, + AllPokemonFullReviveReward, + AllPokemonLevelIncrementReward, + AttackTypeBoosterRewardGenerator, + BaseStatBoosterRewardGenerator, + BerryRewardGenerator, + EvolutionItemRewardGenerator, + FormChangeItemRewardGenerator, + FusePokemonReward, + LapsingTrainerItemReward, + MintRewardGenerator, + PokemonAllMovePpRestoreReward, + PokemonHpRestoreReward, + PokemonLevelIncrementReward, + PokemonPpRestoreReward, + PokemonPpUpReward, + PokemonReviveReward, + PokemonStatusHealReward, + RememberMoveReward, + SpeciesStatBoosterRewardGenerator, + TempStatStageBoosterRewardGenerator, + TeraTypeRewardGenerator, + TmRewardGenerator, +} from "./reward"; + +export function initRewards() { + // Pokeball rewards + allRewards[RewardId.POKEBALL] = () => new AddPokeballReward("pb", PokeballType.POKEBALL, 5, RewardId.POKEBALL); + allRewards[RewardId.GREAT_BALL] = () => new AddPokeballReward("gb", PokeballType.GREAT_BALL, 5, RewardId.GREAT_BALL); + allRewards[RewardId.ULTRA_BALL] = () => new AddPokeballReward("ub", PokeballType.ULTRA_BALL, 5, RewardId.ULTRA_BALL); + allRewards[RewardId.ROGUE_BALL] = () => new AddPokeballReward("rb", PokeballType.ROGUE_BALL, 5, RewardId.ROGUE_BALL); + allRewards[RewardId.MASTER_BALL] = () => + new AddPokeballReward("mb", PokeballType.MASTER_BALL, 1, RewardId.MASTER_BALL); + + // Voucher rewards + allRewards[RewardId.VOUCHER] = () => new AddVoucherReward(VoucherType.REGULAR, 1, RewardId.VOUCHER); + allRewards[RewardId.VOUCHER_PLUS] = () => new AddVoucherReward(VoucherType.PLUS, 1, RewardId.VOUCHER_PLUS); + allRewards[RewardId.VOUCHER_PREMIUM] = () => new AddVoucherReward(VoucherType.PREMIUM, 1, RewardId.VOUCHER_PREMIUM); + + // Money rewards + allRewards[RewardId.NUGGET] = () => + new AddMoneyReward( + "modifierType:ModifierType.NUGGET", + "nugget", + 1, + "modifierType:ModifierType.MoneyRewardModifierType.extra.small", + RewardId.NUGGET, + ); + allRewards[RewardId.BIG_NUGGET] = () => + new AddMoneyReward( + "modifierType:ModifierType.BIG_NUGGET", + "big_nugget", + 2.5, + "modifierType:ModifierType.MoneyRewardModifierType.extra.moderate", + RewardId.BIG_NUGGET, + ); + allRewards[RewardId.RELIC_GOLD] = () => + new AddMoneyReward( + "modifierType:ModifierType.RELIC_GOLD", + "relic_gold", + 10, + "modifierType:ModifierType.MoneyRewardModifierType.extra.large", + RewardId.RELIC_GOLD, + ); + + // Party-wide consumables + allRewards[RewardId.RARER_CANDY] = () => + new AllPokemonLevelIncrementReward("modifierType:ModifierType.RARER_CANDY", "rarer_candy"); + allRewards[RewardId.SACRED_ASH] = () => + new AllPokemonFullReviveReward("modifierType:ModifierType.SACRED_ASH", "sacred_ash"); + + // Pokemon consumables + allRewards[RewardId.RARE_CANDY] = () => + new PokemonLevelIncrementReward("modifierType:ModifierType.RARE_CANDY", "rare_candy"); + + allRewards[RewardId.EVOLUTION_ITEM] = () => new EvolutionItemRewardGenerator(false, RewardId.EVOLUTION_ITEM); + allRewards[RewardId.RARE_EVOLUTION_ITEM] = () => new EvolutionItemRewardGenerator(true, RewardId.RARE_EVOLUTION_ITEM); + + allRewards[RewardId.POTION] = () => + new PokemonHpRestoreReward("modifierType:ModifierType.POTION", "potion", RewardId.POTION, 20, 10); + allRewards[RewardId.SUPER_POTION] = () => + new PokemonHpRestoreReward("modifierType:ModifierType.SUPER_POTION", "super_potion", RewardId.SUPER_POTION, 50, 25); + allRewards[RewardId.HYPER_POTION] = () => + new PokemonHpRestoreReward( + "modifierType:ModifierType.HYPER_POTION", + "hyper_potion", + RewardId.HYPER_POTION, + 200, + 50, + ); + allRewards[RewardId.MAX_POTION] = () => + new PokemonHpRestoreReward("modifierType:ModifierType.MAX_POTION", "max_potion", RewardId.MAX_POTION, 0, 100); + allRewards[RewardId.FULL_RESTORE] = () => + new PokemonHpRestoreReward( + "modifierType:ModifierType.FULL_RESTORE", + "full_restore", + RewardId.FULL_RESTORE, + 0, + 100, + true, + ); + + allRewards[RewardId.REVIVE] = () => + new PokemonReviveReward("modifierType:ModifierType.REVIVE", "revive", RewardId.REVIVE, 50); + allRewards[RewardId.MAX_REVIVE] = () => + new PokemonReviveReward("modifierType:ModifierType.MAX_REVIVE", "max_revive", RewardId.MAX_REVIVE, 100); + + allRewards[RewardId.FULL_HEAL] = () => + new PokemonStatusHealReward("modifierType:ModifierType.FULL_HEAL", "full_heal"); + + allRewards[RewardId.ETHER] = () => + new PokemonPpRestoreReward("modifierType:ModifierType.ETHER", "ether", RewardId.ETHER, 10); + allRewards[RewardId.MAX_ETHER] = () => + new PokemonPpRestoreReward("modifierType:ModifierType.MAX_ETHER", "max_ether", RewardId.MAX_ETHER, -1); + + allRewards[RewardId.ELIXIR] = () => + new PokemonAllMovePpRestoreReward("modifierType:ModifierType.ELIXIR", "elixir", RewardId.ELIXIR, 10); + allRewards[RewardId.MAX_ELIXIR] = () => + new PokemonAllMovePpRestoreReward("modifierType:ModifierType.MAX_ELIXIR", "max_elixir", RewardId.MAX_ELIXIR, -1); + + allRewards[RewardId.PP_UP] = () => + new PokemonPpUpReward("modifierType:ModifierType.PP_UP", "pp_up", RewardId.PP_UP, 1); + allRewards[RewardId.PP_MAX] = () => + new PokemonPpUpReward("modifierType:ModifierType.PP_MAX", "pp_max", RewardId.PP_MAX, 3); + + /*REPEL] = () => new DoubleBattleChanceBoosterReward('Repel', 5), + SUPER_REPEL] = () => new DoubleBattleChanceBoosterReward('Super Repel', 10), + MAX_REPEL] = () => new DoubleBattleChanceBoosterReward('Max Repel', 25),*/ + + allRewards[RewardId.MINT] = () => new MintRewardGenerator(); + + allRewards[RewardId.TERA_SHARD] = () => new TeraTypeRewardGenerator(); + + allRewards[RewardId.TM_COMMON] = () => new TmRewardGenerator(RarityTier.COMMON); + allRewards[RewardId.TM_GREAT] = () => new TmRewardGenerator(RarityTier.GREAT); + allRewards[RewardId.TM_ULTRA] = () => new TmRewardGenerator(RarityTier.ULTRA); + + allRewards[RewardId.MEMORY_MUSHROOM] = () => + new RememberMoveReward("modifierType:ModifierType.MEMORY_MUSHROOM", "big_mushroom"); + + allRewards[RewardId.DNA_SPLICERS] = () => + new FusePokemonReward("modifierType:ModifierType.DNA_SPLICERS", "dna_splicers"); + + // Form change items + allRewards[RewardId.FORM_CHANGE_ITEM] = () => new FormChangeItemRewardGenerator(false, RewardId.FORM_CHANGE_ITEM); + allRewards[RewardId.RARE_FORM_CHANGE_ITEM] = () => + new FormChangeItemRewardGenerator(true, RewardId.RARE_FORM_CHANGE_ITEM); + + // Held items + + allRewards[RewardId.SPECIES_STAT_BOOSTER] = () => new SpeciesStatBoosterRewardGenerator(false); + allRewards[RewardId.RARE_SPECIES_STAT_BOOSTER] = () => new SpeciesStatBoosterRewardGenerator(true); + + allRewards[RewardId.BASE_STAT_BOOSTER] = () => new BaseStatBoosterRewardGenerator(); + + allRewards[RewardId.ATTACK_TYPE_BOOSTER] = () => new AttackTypeBoosterRewardGenerator(); + + allRewards[RewardId.BERRY] = () => new BerryRewardGenerator(); + + // MINI_BLACK_HOLE] = () => new HeldItemReward(HeldItemId.MINI_BLACK_HOLE), + + // Trainer items + + allRewards[RewardId.LURE] = () => new LapsingTrainerItemReward(TrainerItemId.LURE, RewardId.LURE); + allRewards[RewardId.SUPER_LURE] = () => new LapsingTrainerItemReward(TrainerItemId.SUPER_LURE, RewardId.SUPER_LURE); + allRewards[RewardId.MAX_LURE] = () => new LapsingTrainerItemReward(TrainerItemId.MAX_LURE, RewardId.MAX_LURE); + + allRewards[RewardId.TEMP_STAT_STAGE_BOOSTER] = () => new TempStatStageBoosterRewardGenerator(); + + allRewards[RewardId.DIRE_HIT] = () => + new LapsingTrainerItemReward(TrainerItemId.DIRE_HIT, RewardId.TEMP_STAT_STAGE_BOOSTER); + // GOLDEN_POKEBALL] = () => new TrainerItemReward(TrainerItemId.GOLDEN_POKEBALL), +} diff --git a/src/items/all-trainer-items.ts b/src/items/all-trainer-items.ts new file mode 100644 index 00000000000..487bd3224d0 --- /dev/null +++ b/src/items/all-trainer-items.ts @@ -0,0 +1,114 @@ +import { allTrainerItems } from "#data/data-lists"; +import { Stat, type TempBattleStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { + CriticalCatchChanceBoosterTrainerItem, + DoubleBattleChanceBoosterTrainerItem, + EnemyAttackStatusEffectChanceTrainerItem, + EnemyDamageBoosterTrainerItem, + EnemyDamageReducerTrainerItem, + EnemyEndureChanceTrainerItem, + EnemyFusionChanceTrainerItem, + EnemyStatusEffectHealChanceTrainerItem, + EnemyTurnHealTrainerItem, + ExpBoosterTrainerItem, + ExtraRewardTrainerItem, + HealingBoosterTrainerItem, + HealShopCostTrainerItem, + HiddenAbilityChanceBoosterTrainerItem, + LevelIncrementBoosterTrainerItem, + MoneyMultiplierTrainerItem, + PreserveBerryTrainerItem, + ShinyRateBoosterTrainerItem, + TempAccuracyBoosterTrainerItem, + TempCritBoosterTrainerItem, + TempStatStageBoosterTrainerItem, + TrainerItem, + tempStatToTrainerItem, +} from "#items/trainer-item"; + +export function initTrainerItems() { + allTrainerItems[TrainerItemId.MAP] = new TrainerItem(TrainerItemId.MAP, 1); + allTrainerItems[TrainerItemId.IV_SCANNER] = new TrainerItem(TrainerItemId.IV_SCANNER, 1); + allTrainerItems[TrainerItemId.LOCK_CAPSULE] = new TrainerItem(TrainerItemId.LOCK_CAPSULE, 1); + allTrainerItems[TrainerItemId.MEGA_BRACELET] = new TrainerItem(TrainerItemId.MEGA_BRACELET, 1); + allTrainerItems[TrainerItemId.DYNAMAX_BAND] = new TrainerItem(TrainerItemId.DYNAMAX_BAND, 1); + allTrainerItems[TrainerItemId.TERA_ORB] = new TrainerItem(TrainerItemId.TERA_ORB, 1); + + allTrainerItems[TrainerItemId.OVAL_CHARM] = new TrainerItem(TrainerItemId.OVAL_CHARM, 5); + allTrainerItems[TrainerItemId.EXP_SHARE] = new TrainerItem(TrainerItemId.EXP_SHARE, 5); + allTrainerItems[TrainerItemId.EXP_BALANCE] = new TrainerItem(TrainerItemId.EXP_BALANCE, 4); + + allTrainerItems[TrainerItemId.CANDY_JAR] = new LevelIncrementBoosterTrainerItem(TrainerItemId.CANDY_JAR, 99); + allTrainerItems[TrainerItemId.BERRY_POUCH] = new PreserveBerryTrainerItem(TrainerItemId.BERRY_POUCH, 3); + + allTrainerItems[TrainerItemId.HEALING_CHARM] = new HealingBoosterTrainerItem(TrainerItemId.HEALING_CHARM, 0.1, 5); + allTrainerItems[TrainerItemId.EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.EXP_CHARM, 25, 99); + allTrainerItems[TrainerItemId.SUPER_EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.SUPER_EXP_CHARM, 60, 30); + allTrainerItems[TrainerItemId.GOLDEN_EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.GOLDEN_EXP_CHARM, 100, 10); + allTrainerItems[TrainerItemId.AMULET_COIN] = new MoneyMultiplierTrainerItem(TrainerItemId.AMULET_COIN, 5); + + allTrainerItems[TrainerItemId.ABILITY_CHARM] = new HiddenAbilityChanceBoosterTrainerItem( + TrainerItemId.ABILITY_CHARM, + 4, + ); + allTrainerItems[TrainerItemId.GOLDEN_POKEBALL] = new ExtraRewardTrainerItem(TrainerItemId.GOLDEN_POKEBALL, 3); + allTrainerItems[TrainerItemId.SHINY_CHARM] = new ShinyRateBoosterTrainerItem(TrainerItemId.SHINY_CHARM, 4); + allTrainerItems[TrainerItemId.CATCHING_CHARM] = new CriticalCatchChanceBoosterTrainerItem( + TrainerItemId.CATCHING_CHARM, + 3, + ); + + allTrainerItems[TrainerItemId.BLACK_SLUDGE] = new HealShopCostTrainerItem(TrainerItemId.BLACK_SLUDGE, 2.5, 1); + allTrainerItems[TrainerItemId.GOLDEN_BUG_NET] = new TrainerItem(TrainerItemId.GOLDEN_BUG_NET, 1); + + allTrainerItems[TrainerItemId.LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.LURE, 10); + allTrainerItems[TrainerItemId.SUPER_LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.SUPER_LURE, 15); + allTrainerItems[TrainerItemId.MAX_LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.MAX_LURE, 30); + + for (const [statKey, trainerItemType] of Object.entries(tempStatToTrainerItem)) { + const stat = Number(statKey) as TempBattleStat; + if (stat === Stat.ACC) { + allTrainerItems[trainerItemType] = new TempAccuracyBoosterTrainerItem(trainerItemType, 5); + } else { + allTrainerItems[trainerItemType] = new TempStatStageBoosterTrainerItem(trainerItemType, stat, 5); + } + } + allTrainerItems[TrainerItemId.DIRE_HIT] = new TempCritBoosterTrainerItem(TrainerItemId.DIRE_HIT, 5); + + allTrainerItems[TrainerItemId.ENEMY_DAMAGE_BOOSTER] = new EnemyDamageBoosterTrainerItem( + TrainerItemId.ENEMY_DAMAGE_BOOSTER, + ); + allTrainerItems[TrainerItemId.ENEMY_DAMAGE_REDUCTION] = new EnemyDamageReducerTrainerItem( + TrainerItemId.ENEMY_DAMAGE_REDUCTION, + ); + allTrainerItems[TrainerItemId.ENEMY_HEAL] = new EnemyTurnHealTrainerItem(TrainerItemId.ENEMY_HEAL, 10); + allTrainerItems[TrainerItemId.ENEMY_ATTACK_POISON_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem( + TrainerItemId.ENEMY_ATTACK_POISON_CHANCE, + StatusEffect.POISON, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem( + TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE, + StatusEffect.PARALYSIS, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_ATTACK_BURN_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem( + TrainerItemId.ENEMY_ATTACK_BURN_CHANCE, + StatusEffect.BURN, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE] = new EnemyStatusEffectHealChanceTrainerItem( + TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_ENDURE_CHANCE] = new EnemyEndureChanceTrainerItem( + TrainerItemId.ENEMY_ENDURE_CHANCE, + 10, + ); + allTrainerItems[TrainerItemId.ENEMY_FUSED_CHANCE] = new EnemyFusionChanceTrainerItem( + TrainerItemId.ENEMY_FUSED_CHANCE, + 10, + ); +} diff --git a/src/items/apply-trainer-items.ts b/src/items/apply-trainer-items.ts new file mode 100644 index 00000000000..7abeee18df9 --- /dev/null +++ b/src/items/apply-trainer-items.ts @@ -0,0 +1,47 @@ +import { allTrainerItems } from "#data/data-lists"; +import { + type BooleanHolderParams, + type NumberHolderParams, + type PokemonParams, + type PreserveBerryParams, + TrainerItemEffect, +} from "#items/trainer-item"; +import type { TrainerItemManager } from "#items/trainer-item-manager"; + +export type ApplyTrainerItemsParams = { + [TrainerItemEffect.LEVEL_INCREMENT_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.PRESERVE_BERRY]: PreserveBerryParams; + [TrainerItemEffect.HEALING_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.EXP_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.MONEY_MULTIPLIER]: NumberHolderParams; + [TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.SHINY_RATE_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.CRITICAL_CATCH_CHANCE_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.EXTRA_REWARD]: NumberHolderParams; + [TrainerItemEffect.HEAL_SHOP_COST]: NumberHolderParams; + [TrainerItemEffect.DOUBLE_BATTLE_CHANCE_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.TEMP_STAT_STAGE_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.TEMP_ACCURACY_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.TEMP_CRIT_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.ENEMY_DAMAGE_BOOSTER]: NumberHolderParams; + [TrainerItemEffect.ENEMY_DAMAGE_REDUCER]: NumberHolderParams; + [TrainerItemEffect.ENEMY_HEAL]: PokemonParams; + [TrainerItemEffect.ENEMY_ATTACK_STATUS_CHANCE]: PokemonParams; + [TrainerItemEffect.ENEMY_STATUS_HEAL_CHANCE]: PokemonParams; + [TrainerItemEffect.ENEMY_ENDURE_CHANCE]: PokemonParams; + [TrainerItemEffect.ENEMY_FUSED_CHANCE]: BooleanHolderParams; +}; + +export function applyTrainerItems( + effect: T, + manager: TrainerItemManager, + params: ApplyTrainerItemsParams[T], +) { + if (manager) { + for (const item of Object.keys(manager.trainerItems)) { + if (allTrainerItems[item].effects.includes(effect)) { + allTrainerItems[item].apply(manager, params); + } + } + } +} diff --git a/src/items/held-item-data-types.ts b/src/items/held-item-data-types.ts new file mode 100644 index 00000000000..af4a3054bac --- /dev/null +++ b/src/items/held-item-data-types.ts @@ -0,0 +1,93 @@ +// TODO: move all types to `src/@types/` and all functions to a utility place + +import type { FormChangeItem } from "#enums/form-change-item"; +import type { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import type { RarityTier } from "#enums/reward-tier"; +import type { Pokemon } from "#field/pokemon"; + +export type HeldItemData = { + stack: number; + /** + * Whether this item is currently disabled. + * @defaultValue `false` + */ + disabled?: boolean; + /** + * The item's current cooldown. + * @defaultValue `0` + */ + cooldown?: number; +}; + +export type HeldItemDataMap = { + [key in HeldItemId]?: HeldItemData; +}; + +export type HeldItemSpecs = HeldItemData & { + id: HeldItemId; +}; + +export function isHeldItemSpecs(entry: any): entry is HeldItemSpecs { + return typeof entry.id === "number" && "stack" in entry; +} + +// Types used for form change items +interface FormChangeItemData { + active: boolean; +} + +export type FormChangeItemPropertyMap = { + [key in FormChangeItem]?: FormChangeItemData; +}; + +export type FormChangeItemSpecs = FormChangeItemData & { + id: FormChangeItem; +}; + +export function isFormChangeItemSpecs(entry: any): entry is FormChangeItemSpecs { + return typeof entry.id === "number" && "active" in entry; +} + +export type HeldItemWeights = { + [key in HeldItemId]?: number; +}; + +export type HeldItemWeightFunc = (party: Pokemon[]) => number; + +export type HeldItemCategoryEntry = HeldItemData & { + id: HeldItemCategoryId; + customWeights?: HeldItemWeights; +}; + +export function isHeldItemCategoryEntry(entry: any): entry is HeldItemCategoryEntry { + return entry?.id && isHeldItemCategoryEntry(entry.id) && "customWeights" in entry; +} + +type HeldItemPoolEntry = { + entry: HeldItemId | HeldItemCategoryId | HeldItemCategoryEntry | HeldItemSpecs; + weight: number | HeldItemWeightFunc; +}; + +export type HeldItemPool = HeldItemPoolEntry[]; + +export function isHeldItemPool(value: any): value is HeldItemPool { + return Array.isArray(value) && value.every(entry => "entry" in entry && "weight" in entry); +} + +export type HeldItemTieredPool = { + [key in RarityTier]?: HeldItemPool; +}; + +type HeldItemConfigurationEntry = { + entry: HeldItemId | HeldItemCategoryId | HeldItemCategoryEntry | HeldItemSpecs | HeldItemPool | FormChangeItemSpecs; + count?: number | (() => number); +}; + +export type HeldItemConfiguration = HeldItemConfigurationEntry[]; + +export type PokemonItemMap = { + item: HeldItemSpecs | FormChangeItemSpecs; + pokemonId: number; +}; + +export type HeldItemSaveData = (HeldItemSpecs | FormChangeItemSpecs)[]; diff --git a/src/items/held-item-default-tiers.ts b/src/items/held-item-default-tiers.ts new file mode 100644 index 00000000000..8ca4c286343 --- /dev/null +++ b/src/items/held-item-default-tiers.ts @@ -0,0 +1,47 @@ +import { getHeldItemCategory, HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { RarityTier } from "#enums/reward-tier"; + +export const heldItemRarities = { + [HeldItemCategoryId.BERRY]: RarityTier.COMMON, + + [HeldItemCategoryId.BASE_STAT_BOOST]: RarityTier.GREAT, + [HeldItemId.WHITE_HERB]: RarityTier.GREAT, + [HeldItemId.METAL_POWDER]: RarityTier.GREAT, + [HeldItemId.QUICK_POWDER]: RarityTier.GREAT, + [HeldItemId.DEEP_SEA_SCALE]: RarityTier.GREAT, + [HeldItemId.DEEP_SEA_TOOTH]: RarityTier.GREAT, + [HeldItemId.SOOTHE_BELL]: RarityTier.GREAT, + + [HeldItemCategoryId.TYPE_ATTACK_BOOSTER]: RarityTier.ULTRA, + [HeldItemId.REVIVER_SEED]: RarityTier.ULTRA, + [HeldItemId.LIGHT_BALL]: RarityTier.ULTRA, + [HeldItemId.EVIOLITE]: RarityTier.ULTRA, + [HeldItemId.QUICK_CLAW]: RarityTier.ULTRA, + [HeldItemId.MYSTICAL_ROCK]: RarityTier.ULTRA, + [HeldItemId.WIDE_LENS]: RarityTier.ULTRA, + [HeldItemId.GOLDEN_PUNCH]: RarityTier.ULTRA, + [HeldItemId.TOXIC_ORB]: RarityTier.ULTRA, + [HeldItemId.FLAME_ORB]: RarityTier.ULTRA, + [HeldItemId.LUCKY_EGG]: RarityTier.ULTRA, + + [HeldItemId.FOCUS_BAND]: RarityTier.ROGUE, + [HeldItemId.KINGS_ROCK]: RarityTier.ROGUE, + [HeldItemId.LEFTOVERS]: RarityTier.ROGUE, + [HeldItemId.SHELL_BELL]: RarityTier.ROGUE, + [HeldItemId.GRIP_CLAW]: RarityTier.ROGUE, + [HeldItemId.SOUL_DEW]: RarityTier.ROGUE, + [HeldItemId.BATON]: RarityTier.ROGUE, + [HeldItemId.GOLDEN_EGG]: RarityTier.ULTRA, + + [HeldItemId.MINI_BLACK_HOLE]: RarityTier.MASTER, + [HeldItemId.MULTI_LENS]: RarityTier.MASTER, +}; + +export function getHeldItemTier(item: HeldItemId): RarityTier { + let tier = heldItemRarities[item]; + if (!tier) { + const category = getHeldItemCategory(item); + tier = heldItemRarities[category]; + } + return tier ?? RarityTier.LUXURY; +} diff --git a/src/items/held-item-manager.ts b/src/items/held-item-manager.ts new file mode 100644 index 00000000000..2c2a084ed15 --- /dev/null +++ b/src/items/held-item-manager.ts @@ -0,0 +1,238 @@ +import { allHeldItems } from "#data/data-lists"; +import type { FormChangeItem } from "#enums/form-change-item"; +import { + type HeldItemCategoryId, + type HeldItemId, + isCategoryId, + isItemInCategory, + isItemInRequested, +} from "#enums/held-item-id"; +import { + type FormChangeItemPropertyMap, + type FormChangeItemSpecs, + type HeldItemConfiguration, + type HeldItemDataMap, + type HeldItemSaveData, + type HeldItemSpecs, + isHeldItemSpecs, +} from "#items/held-item-data-types"; +import { getTypedEntries, getTypedKeys } from "#utils/common"; + +export class HeldItemManager { + 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 [id, item] of getTypedEntries(this.heldItems)) { + if (item && (!restrictedIds || id in restrictedIds)) { + const specs: HeldItemSpecs = { ...item, id }; + config.push({ entry: specs, count: 1 }); + } + } + for (const [id, item] of getTypedEntries(this.formChangeItems)) { + if (item) { + const specs: FormChangeItemSpecs = { ...item, id }; + config.push({ entry: specs, count: 1 }); + } + } + return config; + } + + generateSaveData(): HeldItemSaveData { + const saveData: HeldItemSaveData = []; + for (const [id, item] of getTypedEntries(this.heldItems)) { + if (item) { + const specs: HeldItemSpecs = { ...item, id }; + saveData.push(specs); + } + } + for (const [id, item] of getTypedEntries(this.formChangeItems)) { + if (item) { + const specs: FormChangeItemSpecs = { ...item, id }; + saveData.push(specs); + } + } + return saveData; + } + + getHeldItems(): HeldItemId[] { + return getTypedKeys(this.heldItems); + } + + getTransferableHeldItems(): HeldItemId[] { + return getTypedKeys(this.heldItems).filter(k => allHeldItems[k].isTransferable); + } + + getStealableHeldItems(): HeldItemId[] { + return getTypedKeys(this.heldItems).filter(k => allHeldItems[k].isStealable); + } + + getSuppressableHeldItems(): HeldItemId[] { + return getTypedKeys(this.heldItems).filter(k => allHeldItems[k].isSuppressable); + } + + hasItem(itemType: HeldItemId | HeldItemCategoryId): boolean { + if (isCategoryId(itemType)) { + return getTypedKeys(this.heldItems).some(id => isItemInCategory(id, itemType as HeldItemCategoryId)); + } + return itemType in this.heldItems; + } + + hasTransferableItem(itemType: HeldItemId | HeldItemCategoryId): boolean { + if (isCategoryId(itemType)) { + return getTypedKeys(this.heldItems).some( + id => isItemInCategory(id, itemType as HeldItemCategoryId) && allHeldItems[id].isTransferable, + ); + } + return itemType in this.heldItems && allHeldItems[itemType].isTransferable; + } + + getStack(itemType: HeldItemId): number { + const item = this.heldItems[itemType]; + return item ? item.stack : 0; + } + + // Use for tests if necessary to go over stack limit + setStack(itemType: HeldItemId, stack: number): void { + const item = this.heldItems[itemType]; + if (item) { + item.stack = stack; + } + } + + isMaxStack(itemType: HeldItemId): boolean { + const item = this.heldItems[itemType]; + return item ? item.stack >= allHeldItems[itemType].getMaxStackCount() : false; + } + + overrideItems(newItems: HeldItemDataMap) { + this.heldItems = newItems; + // The following is to allow randomly generated item configs to have stack 0 + for (const [item, properties] of getTypedEntries(this.heldItems)) { + if (!properties || properties.stack <= 0) { + delete this.heldItems[item]; + } + } + } + + add(itemType: HeldItemId | HeldItemSpecs, addStack = 1): boolean { + if (isHeldItemSpecs(itemType)) { + return this.addItemWithSpecs(itemType); + } + + const maxStack = allHeldItems[itemType].getMaxStackCount(); + const item = this.heldItems[itemType]; + + if (item) { + // TODO: We may want an error message of some kind instead + if (item.stack < maxStack) { + item.stack = Math.min(item.stack + addStack, maxStack); + return true; + } + } else { + this.heldItems[itemType] = { stack: Math.min(addStack, maxStack) }; + return true; + } + return false; + } + + addItemWithSpecs(itemSpecs: HeldItemSpecs): boolean { + const id = itemSpecs.id; + const maxStack = allHeldItems[id].getMaxStackCount(); + const item = this.heldItems[id]; + + const tempStack = item?.stack ?? 0; + + this.heldItems[id] = itemSpecs; + this.heldItems[id].stack = Math.min(itemSpecs.stack + tempStack, maxStack); + + return true; + } + + remove(itemType: HeldItemId, removeStack = 1, all = false) { + const item = this.heldItems[itemType]; + + if (item) { + item.stack -= removeStack; + + if (all || item.stack <= 0) { + delete this.heldItems[itemType]; + } + } + } + + filterRequestedItems(requestedItems: (HeldItemCategoryId | HeldItemId)[], transferableOnly = true, exclude = false) { + const currentItems = transferableOnly ? this.getTransferableHeldItems() : this.getHeldItems(); + return currentItems.filter(it => !exclude && isItemInRequested(it, requestedItems)); + } + + getHeldItemCount(): number { + let total = 0; + for (const properties of Object.values(this.heldItems)) { + total += properties?.stack ?? 0; + } + return total; + } + + addFormChangeItem(id: FormChangeItem) { + if (!(id in this.formChangeItems)) { + this.formChangeItems[id] = { active: false }; + } + } + + addFormChangeItemWithSpecs(item: FormChangeItemSpecs) { + if (!(item.id in this.formChangeItems)) { + this.formChangeItems[item.id] = { active: item.active }; + } + } + + hasFormChangeItem(id: FormChangeItem): boolean { + return id in this.formChangeItems; + } + + hasActiveFormChangeItem(id: FormChangeItem): boolean { + const item = this.formChangeItems[id]; + if (item) { + return item.active; + } + return false; + } + + getFormChangeItems(): FormChangeItem[] { + return getTypedKeys(this.formChangeItems).map(k => 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/items/held-item-pool.ts b/src/items/held-item-pool.ts new file mode 100644 index 00000000000..55066a86ede --- /dev/null +++ b/src/items/held-item-pool.ts @@ -0,0 +1,324 @@ +import { allHeldItems } from "#data/data-lists"; +import { BerryType } from "#enums/berry-type"; +import { HeldItemCategoryId, HeldItemId, HeldItemNames, isCategoryId } from "#enums/held-item-id"; +import type { PokemonType } from "#enums/pokemon-type"; +import { HeldItemPoolType } from "#enums/reward-pool-type"; +import { RarityTier } from "#enums/reward-tier"; +import { PERMANENT_STATS } from "#enums/stat"; +import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; +import { attackTypeToHeldItem } from "#items/attack-type-booster"; +import { permanentStatToHeldItem } from "#items/base-stat-booster"; +import { berryTypeToHeldItem } from "#items/berry"; +import { + type HeldItemConfiguration, + type HeldItemPool, + type HeldItemSaveData, + type HeldItemSpecs, + type HeldItemTieredPool, + type HeldItemWeights, + isFormChangeItemSpecs, + isHeldItemCategoryEntry, + isHeldItemPool, + isHeldItemSpecs, +} from "#items/held-item-data-types"; +import { coerceArray, isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#utils/common"; +import { getEnumValues } from "#utils/enums"; + +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 = getDailyRarityTier(tierValue); + + const item = getNewHeldItemFromPool( + getHeldItemPool(HeldItemPoolType.DAILY_STARTER)[tier] as HeldItemPool, + p, + party, + ); + p.heldItemManager.add(item); + } + } +} + +function getDailyRarityTier(tierValue: number): RarityTier { + if (tierValue > 25) { + return RarityTier.COMMON; + } + if (tierValue > 12) { + return RarityTier.GREAT; + } + if (tierValue > 4) { + return RarityTier.ULTRA; + } + if (tierValue > 0) { + return RarityTier.ROGUE; + } + return RarityTier.MASTER; +} + +function getHeldItemPool(poolType: HeldItemPoolType): HeldItemTieredPool { + let pool: HeldItemTieredPool; + switch (poolType) { + case HeldItemPoolType.WILD: + pool = wildHeldItemPool; + break; + case HeldItemPoolType.TRAINER: + pool = trainerHeldItemPool; + break; + case HeldItemPoolType.DAILY_STARTER: + pool = dailyStarterHeldItemPool; + break; + } + return pool; +} + +// TODO: Add proper documentation to this function (once it fully works...) +export function assignEnemyHeldItemsForWave( + waveIndex: number, + count: number, + enemy: EnemyPokemon, + poolType: HeldItemPoolType.WILD | HeldItemPoolType.TRAINER, + upgradeChance = 0, +): void { + for (let i = 1; i <= count; i++) { + const item = getNewHeldItemFromTieredPool( + getHeldItemPool(poolType), + enemy, + upgradeChance && !randSeedInt(upgradeChance) ? 1 : 0, + ); + if (item) { + enemy.heldItemManager.add(item); + } + } + if (!(waveIndex % 1000)) { + enemy.heldItemManager.add(HeldItemId.MINI_BLACK_HOLE); + } +} + +function getRandomTier(): RarityTier { + const tierValue = randSeedInt(1024); + + if (tierValue > 255) { + return RarityTier.COMMON; + } + if (tierValue > 60) { + return RarityTier.GREAT; + } + if (tierValue > 12) { + return RarityTier.ULTRA; + } + if (tierValue) { + return RarityTier.ROGUE; + } + return RarityTier.MASTER; +} + +function determineItemPoolTier(pool: HeldItemTieredPool, upgradeCount?: number): RarityTier { + let tier = getRandomTier(); + + if (!upgradeCount) { + upgradeCount = 0; + } + + tier += upgradeCount; + while (tier && !pool[tier]?.length) { + tier--; + if (upgradeCount) { + upgradeCount--; + } + } + + return tier; +} + +function getNewHeldItemFromTieredPool( + pool: HeldItemTieredPool, + pokemon: Pokemon, + upgradeCount: number, +): HeldItemId | HeldItemSpecs { + const tier = determineItemPoolTier(pool, upgradeCount); + const tierPool = pool[tier]; + + return getNewHeldItemFromPool(tierPool!, pokemon); +} + +export function getNewVitaminHeldItem(customWeights: HeldItemWeights = {}, target?: Pokemon): HeldItemId { + const items = PERMANENT_STATS.map(s => permanentStatToHeldItem[s]); + const weights = items.map(t => (target?.heldItemManager.isMaxStack(t) ? 0 : (customWeights[t] ?? 1))); + const pickedIndex = pickWeightedIndex(weights); + return !isNullOrUndefined(pickedIndex) ? items[pickedIndex] : 0; +} + +export function getNewBerryHeldItem(customWeights: HeldItemWeights = {}, target?: Pokemon): HeldItemId { + const berryTypes = getEnumValues(BerryType); + const items = berryTypes.map(b => berryTypeToHeldItem[b]); + + const weights = items.map(t => + target?.heldItemManager.isMaxStack(t) + ? 0 + : (customWeights[t] ?? + (t === HeldItemId.SITRUS_BERRY || t === HeldItemId.LUM_BERRY || t === HeldItemId.LEPPA_BERRY)) + ? 2 + : 1, + ); + + const pickedIndex = pickWeightedIndex(weights); + return !isNullOrUndefined(pickedIndex) ? items[pickedIndex] : 0; +} + +export function getNewAttackTypeBoosterHeldItem( + pokemon: Pokemon | Pokemon[], + customWeights: HeldItemWeights = {}, + target?: Pokemon, +): HeldItemId | null { + const party = coerceArray(pokemon); + + // TODO: make this consider moves or abilities that change types + const attackMoveTypes = party.flatMap(p => + p + .getMoveset() + .filter(m => m.getMove().is("AttackMove")) + .map(m => p.getMoveType(m.getMove(), true)), + ); + if (!attackMoveTypes.length) { + return null; + } + + const attackMoveTypeWeights = attackMoveTypes.reduce((map, type) => { + const current = map.get(type) ?? 0; + if (current < 3) { + map.set(type, current + 1); + } + return map; + }, new Map()); + + const types = Array.from(attackMoveTypeWeights.keys()); + + const weights = types.map(type => + target?.heldItemManager.isMaxStack(attackTypeToHeldItem[type]) + ? 0 + : (customWeights[attackTypeToHeldItem[type]] ?? attackMoveTypeWeights.get(type)!), + ); + + const pickedIndex = pickWeightedIndex(weights); + return !isNullOrUndefined(pickedIndex) ? attackTypeToHeldItem[types[pickedIndex]] : 0; +} + +export function getNewHeldItemFromCategory( + id: HeldItemCategoryId, + pokemon: Pokemon | Pokemon[], + customWeights: HeldItemWeights = {}, + target?: Pokemon, +): HeldItemId | null { + if (id === HeldItemCategoryId.BERRY) { + return getNewBerryHeldItem(customWeights, target); + } + if (id === HeldItemCategoryId.VITAMIN) { + return getNewVitaminHeldItem(customWeights, target); + } + if (id === HeldItemCategoryId.TYPE_ATTACK_BOOSTER) { + return getNewAttackTypeBoosterHeldItem(pokemon, customWeights, target); + } + return null; +} + +function getPoolWeights(pool: HeldItemPool, pokemon: Pokemon): number[] { + return pool.map(p => { + let weight = typeof p.weight === "function" ? p.weight(coerceArray(pokemon)) : p.weight; + + if (typeof p.entry === "number" && !isCategoryId(p.entry)) { + const itemId = p.entry as HeldItemId; + console.log("ITEM ID: ", itemId, HeldItemNames[itemId]); + console.log(allHeldItems[itemId]); + + if (pokemon.heldItemManager.getStack(itemId) >= allHeldItems[itemId].getMaxStackCount()) { + weight = 0; + } + } + + return weight; + }); +} + +function getNewHeldItemFromPool(pool: HeldItemPool, pokemon: Pokemon, party?: Pokemon[]): HeldItemId | HeldItemSpecs { + const weights = getPoolWeights(pool, pokemon); + + const pickedIndex = pickWeightedIndex(weights); + if (isNullOrUndefined(pickedIndex)) { + return 0; + } + const entry = pool[pickedIndex].entry; + + if (typeof entry === "number") { + if (isCategoryId(entry)) { + return getNewHeldItemFromCategory(entry, party ?? pokemon, {}, pokemon) as HeldItemId; + } + return entry as HeldItemId; + } + + if (isHeldItemCategoryEntry(entry)) { + return getNewHeldItemFromCategory(entry.id, party ?? pokemon, entry?.customWeights, pokemon) as HeldItemId; + } + + return entry as HeldItemSpecs; +} + +function assignItemsFromCategory(id: HeldItemCategoryId, pokemon: Pokemon, count: number) { + for (let i = 1; i <= count; i++) { + const newItem = getNewHeldItemFromCategory(id, pokemon, {}, pokemon); + if (newItem) { + pokemon.heldItemManager.add(newItem); + } + } +} + +export function assignItemsFromConfiguration(config: HeldItemConfiguration, pokemon: Pokemon) { + config.forEach(item => { + const { entry, count } = item; + const actualCount = typeof count === "function" ? count() : (count ?? 1); + + if (typeof entry === "number") { + if (isCategoryId(entry)) { + assignItemsFromCategory(entry, pokemon, actualCount); + } + pokemon.heldItemManager.add(entry as HeldItemId, actualCount); + } + + if (isHeldItemSpecs(entry)) { + pokemon.heldItemManager.add(entry); + } + + if (isFormChangeItemSpecs(entry)) { + pokemon.heldItemManager.addFormChangeItemWithSpecs(entry); + } + + if (isHeldItemCategoryEntry(entry)) { + assignItemsFromCategory(entry.id, pokemon, actualCount); + } + + if (isHeldItemPool(entry)) { + for (let i = 1; i <= actualCount; i++) { + const newItem = getNewHeldItemFromPool(entry, pokemon); + if (newItem) { + pokemon.heldItemManager.add(newItem); + } + } + } + }); +} + +// TODO: Handle form change items +export function saveDataToConfig(saveData: HeldItemSaveData): HeldItemConfiguration { + const config: HeldItemConfiguration = []; + for (const specs of saveData) { + config.push({ entry: specs, count: 1 }); + } + return config; +} diff --git a/src/items/held-item.ts b/src/items/held-item.ts new file mode 100644 index 00000000000..6dd76883e47 --- /dev/null +++ b/src/items/held-item.ts @@ -0,0 +1,139 @@ +import { applyAbAttrs } from "#abilities/apply-ab-attrs"; +import { globalScene } from "#app/global-scene"; +import { type HeldItemId, HeldItemNames } from "#enums/held-item-id"; +import type { Pokemon } from "#field/pokemon"; +import i18next from "i18next"; + +export class HeldItem { + // public pokemonId: number; + // TODO: Should this be readonly? + public type: HeldItemId; + public readonly maxStackCount: number; + public isTransferable = true; + public isStealable = true; + public isSuppressable = true; + + //TODO: If this is actually never changed by any subclass, perhaps it should not be here + public soundName = "se/restore"; + + constructor(type: HeldItemId, maxStackCount = 1) { + this.type = type; + this.maxStackCount = maxStackCount; + + this.isTransferable = true; + this.isStealable = true; + this.isSuppressable = true; + } + + get name(): string { + return i18next.t(`modifierType:ModifierType.${HeldItemNames[this.type]}.name`); + } + + get description(): string { + return i18next.t(`modifierType:ModifierType.${HeldItemNames[this.type]}.description`); + } + + get iconName(): string { + return `${HeldItemNames[this.type]?.toLowerCase()}`; + } + + // TODO: Aren't these fine as just properties to set in the subclass definition? + untransferable(): HeldItem { + this.isTransferable = false; + return this; + } + + unstealable(): HeldItem { + this.isStealable = false; + return this; + } + + unsuppressable(): HeldItem { + this.isSuppressable = false; + return this; + } + + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114950716 + getMaxStackCount(): number { + return this.maxStackCount; + } + + createSummaryIcon(pokemon?: Pokemon, overrideStackCount?: number): Phaser.GameObjects.Container { + const stackCount = overrideStackCount ?? (pokemon ? this.getStackCount(pokemon) : 0); + + const container = globalScene.add.container(0, 0); + + const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5); + container.add(item); + + const stackText = this.getIconStackText(stackCount); + if (stackText) { + container.add(stackText); + } + + container.setScale(0.5); + + return container; + } + + createPokemonIcon(pokemon: Pokemon): Phaser.GameObjects.Container { + const container = globalScene.add.container(0, 0); + + const pokemonIcon = globalScene.addPokemonIcon(pokemon, -2, 10, 0, 0.5, undefined, true); + container.add(pokemonIcon); + container.setName(pokemon.id.toString()); + + const item = globalScene.add + .sprite(16, 16, "items") + .setScale(0.5) + .setOrigin(0, 0.5) + .setTexture("items", this.iconName); + container.add(item); + + const stackText = this.getIconStackText(this.getStackCount(pokemon)); + if (stackText) { + container.add(stackText); + } + + return container; + } + + getIconStackText(stackCount: number): Phaser.GameObjects.BitmapText | null { + if (this.getMaxStackCount() === 1) { + return null; + } + + const text = globalScene.add.bitmapText(10, 15, "item-count", stackCount.toString(), 11); + text.letterSpacing = -0.5; + if (stackCount >= this.getMaxStackCount()) { + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114955458 + text.setTint(0xf89890); + } + text.setOrigin(0); + + return text; + } + + getStackCount(pokemon: Pokemon): number { + const stackCount = pokemon.heldItemManager.getStack(this.type); + return stackCount; + } + + getScoreMultiplier(): number { + return 1; + } +} + +export class ConsumableHeldItem extends HeldItem { + // Sometimes berries are not eaten, some stuff may not proc unburden... + consume(pokemon: Pokemon, isPlayer: boolean, remove = true, unburden = true): void { + if (remove) { + pokemon.heldItemManager.remove(this.type, 1); + // TODO: Turn this into updateItemBar or something + globalScene.updateItems(isPlayer); + } + if (unburden) { + applyAbAttrs("PostItemLostAbAttr", { pokemon: pokemon }); + } + } +} diff --git a/src/items/held-items/accuracy-booster.ts b/src/items/held-items/accuracy-booster.ts new file mode 100644 index 00000000000..ff57a82b192 --- /dev/null +++ b/src/items/held-items/accuracy-booster.ts @@ -0,0 +1,46 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; + +export interface AccuracyBoostParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Holds the move's accuracy, which may be modified after item application */ + moveAccuracy: NumberHolder; +} + +/** + * @sealed + */ +export class AccuracyBoosterHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.ACCURACY_BOOSTER]; + + private accuracyAmount: number; + + constructor(type: HeldItemId, maxStackCount: number, accuracy: number) { + super(type, maxStackCount); + this.accuracyAmount = accuracy; + } + + /** + * Checks if {@linkcode PokemonMoveAccuracyBoosterHeldItem} 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 PokemonMoveAccuracyBoosterHeldItem} should be applied + */ + // override shouldApply(pokemon?: Pokemon, moveAccuracy?: NumberHolder): boolean { + // return super.shouldApply(pokemon, moveAccuracy) && !!moveAccuracy; + // } + + /** + * Applies {@linkcode PokemonMoveAccuracyBoosterHeldItem} + */ + apply({ pokemon, moveAccuracy }: AccuracyBoostParams): true { + const stackCount = pokemon.heldItemManager.getStack(this.type); + 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..f1558d095a8 --- /dev/null +++ b/src/items/held-items/attack-type-booster.ts @@ -0,0 +1,76 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId, HeldItemNames } from "#enums/held-item-id"; +import { PokemonType } from "#enums/pokemon-type"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; +import i18next from "i18next"; + +export interface AttackTypeBoostParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The resolved type of the move */ + moveType: PokemonType; + /** Holder for the damage value */ + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2119660807 + movePower: NumberHolder; +} + +interface AttackTypeToHeldItemMap { + [key: number]: HeldItemId; +} + +export const attackTypeToHeldItem: AttackTypeToHeldItemMap = { + [PokemonType.NORMAL]: HeldItemId.SILK_SCARF, + [PokemonType.FIGHTING]: HeldItemId.BLACK_BELT, + [PokemonType.FLYING]: HeldItemId.SHARP_BEAK, + [PokemonType.POISON]: HeldItemId.POISON_BARB, + [PokemonType.GROUND]: HeldItemId.SOFT_SAND, + [PokemonType.ROCK]: HeldItemId.HARD_STONE, + [PokemonType.BUG]: HeldItemId.SILVER_POWDER, + [PokemonType.GHOST]: HeldItemId.SPELL_TAG, + [PokemonType.STEEL]: HeldItemId.METAL_COAT, + [PokemonType.FIRE]: HeldItemId.CHARCOAL, + [PokemonType.WATER]: HeldItemId.MYSTIC_WATER, + [PokemonType.GRASS]: HeldItemId.MIRACLE_SEED, + [PokemonType.ELECTRIC]: HeldItemId.MAGNET, + [PokemonType.PSYCHIC]: HeldItemId.TWISTED_SPOON, + [PokemonType.ICE]: HeldItemId.NEVER_MELT_ICE, + [PokemonType.DRAGON]: HeldItemId.DRAGON_FANG, + [PokemonType.DARK]: HeldItemId.BLACK_GLASSES, + [PokemonType.FAIRY]: HeldItemId.FAIRY_FEATHER, +}; + +export class AttackTypeBoosterHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.TURN_END_HEAL]; + public moveType: PokemonType; + public powerBoost: number; + + // This constructor may need a revision + constructor(type: HeldItemId, maxStackCount: number, moveType: PokemonType, powerBoost: number) { + super(type, maxStackCount); + this.moveType = moveType; + this.powerBoost = powerBoost; + } + + get name(): string { + return i18next.t(`modifierType:AttackTypeBoosterItem.${HeldItemNames[this.type]?.toLowerCase()}`); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.AttackTypeBoosterModifierType.description", { + moveType: i18next.t(`pokemonInfo:Type.${PokemonType[this.moveType]}`), + }); + } + + get iconName(): string { + return `${HeldItemNames[this.type]?.toLowerCase()}`; + } + + apply({ pokemon, moveType, movePower }: AttackTypeBoostParams): void { + 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..03be45ec4b9 --- /dev/null +++ b/src/items/held-items/base-stat-booster.ts @@ -0,0 +1,78 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; +import { getStatKey, type PermanentStat, Stat } from "#enums/stat"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import i18next from "i18next"; + +export interface BaseStatBoosterParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The base stats of the {@linkcode pokemon} */ + baseStats: number[]; +} + +type PermanentStatToHeldItemMap = { + [key in PermanentStat]: 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: HeldItemEffect[] = [HeldItemEffect.BASE_STAT_BOOSTER]; + public stat: PermanentStat; + + constructor(type: HeldItemId, maxStackCount: number, 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}. + */ + apply({ pokemon, baseStats }: BaseStatBoosterParams): boolean { + const stackCount = pokemon.heldItemManager.getStack(this.type); + baseStats[this.stat] = Math.floor(baseStats[this.stat] * (1 + stackCount * 0.1)); + return true; + } +} diff --git a/src/items/held-items/base-stat-flat.ts b/src/items/held-items/base-stat-flat.ts new file mode 100644 index 00000000000..d0c622af6ba --- /dev/null +++ b/src/items/held-items/base-stat-flat.ts @@ -0,0 +1,64 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import { Stat } from "#enums/stat"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import i18next from "i18next"; + +export interface BaseStatFlatParams { + /** 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: HeldItemEffect[] = [HeldItemEffect.BASE_STAT_FLAT]; + public isTransferable = false; + + get description(): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description"); + } + + /** + * Checks if the {@linkcode PokemonBaseStatFlatModifier} should be applied to the {@linkcode Pokemon}. + * @param pokemon The {@linkcode Pokemon} that holds the item + * @param baseStats The base stats of the {@linkcode Pokemon} + * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { + // return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); + // } + + /** + * Applies the {@linkcode PokemonBaseStatFlatModifier} + */ + apply({ pokemon, baseStats }: BaseStatFlatParams): true { + const stats = this.getStats(pokemon); + const statModifier = 20; + // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats + baseStats.forEach((v, i) => { + if (stats.includes(i)) { + const newVal = Math.floor(v + statModifier); + baseStats[i] = Math.min(Math.max(newVal, 1), 999999); + } + }); + + return true; + } + + /** + * Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef + * @returns Array of 3 {@linkcode Stat}s to boost + */ + getStats(pokemon: Pokemon): [HpOrSpeed: Stat, AtkOrSpAtk: Stat, DefOrSpDef: Stat] { + const baseStats = pokemon.getSpeciesForm().baseStats.slice(0); + return [ + baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD, + baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK, + baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF, + ]; + } +} 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..dce4355a009 --- /dev/null +++ b/src/items/held-items/base-stat-total.ts @@ -0,0 +1,70 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import i18next from "i18next"; + +export interface BaseStatTotalParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Array of the pokemon's base stat; modified in place after item application */ + baseStats: number[]; +} + +/** + * Currently used by Shuckle Juice item + */ +export class BaseStatTotalHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.BASE_STAT_TOTAL]; + public isTransferable = false; + public statModifier: number; + + constructor(type: HeldItemId, maxStackCount: number, statModifier: number) { + super(type, maxStackCount); + this.statModifier = statModifier; + } + + get name(): string { + return this.statModifier > 0 + ? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.name") + : i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.name"); + } + + // TODO: where is this description shown? + get description(): string { + return this.statModifier > 0 + ? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.description") + : i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.description"); + } + + get iconName(): string { + return this.statModifier > 0 ? "berry_juice_good" : "berry_juice_bad"; + } + + /** + * Checks if {@linkcode PokemonBaseStatTotalModifier} should be applied to the specified {@linkcode Pokemon}. + * @param pokemon the {@linkcode Pokemon} to be modified + * @param baseStats the base stats of the {@linkcode Pokemon} + * @returns `true` if the {@linkcode Pokemon} should be modified + */ + // override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { + // return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); + // } + + /** + * Applies the {@linkcode PokemonBaseStatTotalModifier} + * @param _pokemon the {@linkcode Pokemon} to be modified + * @param baseStats the base stats of the {@linkcode Pokemon} + * @returns always `true` + */ + apply({ baseStats }: BaseStatTotalParams): true { + // Modifies the passed in baseStats[] array + baseStats.forEach((v, i) => { + // HP is affected by half as much as other stats + const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier); + baseStats[i] = Math.min(Math.max(newVal, 1), 999999); + }); + + return true; + } +} diff --git a/src/items/held-items/baton.ts b/src/items/held-items/baton.ts new file mode 100644 index 00000000000..72c89727a73 --- /dev/null +++ b/src/items/held-items/baton.ts @@ -0,0 +1,23 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; + +export interface BatonParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The amount of exp to gain */ + expAmount: NumberHolder; +} + +export class BatonHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.BATON]; + + /** + * Applies {@linkcode SwitchEffectTransferModifier} + * @returns always `true` + */ + apply(): true { + return true; + } +} diff --git a/src/items/held-items/berry.ts b/src/items/held-items/berry.ts new file mode 100644 index 00000000000..e0903778965 --- /dev/null +++ b/src/items/held-items/berry.ts @@ -0,0 +1,96 @@ +import { globalScene } from "#app/global-scene"; +import { getBerryEffectDescription, getBerryEffectFunc, getBerryName, getBerryPredicate } from "#data/berry"; +import { BerryType } from "#enums/berry-type"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; +import { BerryUsedEvent } from "#events/battle-scene"; +import type { Pokemon } from "#field/pokemon"; +import { ConsumableHeldItem } from "#items/held-item"; +import { TrainerItemEffect } from "#items/trainer-item"; +import type { ObjectValues } from "#types/type-helpers"; +import { BooleanHolder } from "#utils/common"; + +type BerryTypeToHeldItemMap = { + [key in ObjectValues]: HeldItemId; +}; + +// TODO: Rework this to use a bitwise XOR +export const berryTypeToHeldItem = { + [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, +} satisfies BerryTypeToHeldItemMap; + +export interface BerryParams { + /** The pokemon with the berry */ + pokemon: Pokemon; +} + +// TODO: Maybe split up into subclasses? +export class BerryHeldItem extends ConsumableHeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.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} + * @returns always `true` + */ + apply({ pokemon }: BerryParams): boolean { + if (!this.shouldApply(pokemon)) { + return false; + } + + const preserve = new BooleanHolder(false); + globalScene.applyPlayerItems(TrainerItemEffect.PRESERVE_BERRY, { pokemon: pokemon, doPreserve: preserve }); + const consumed = !preserve.value; + + // munch the berry and trigger unburden-like effects + getBerryEffectFunc(this.berryType)(pokemon); + this.consume(pokemon, pokemon.isPlayer(), consumed); + + // TODO: Update this method to work with held items + // Update berry eaten trackers for Belch, Harvest, Cud Chew, etc. + // Don't recover it if we proc berry pouch (no item duplication) + pokemon.recordEatenBerry(this.berryType, consumed); + + globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(pokemon, this.berryType)); + + return true; + } +} diff --git a/src/items/held-items/bypass-speed-chance.ts b/src/items/held-items/bypass-speed-chance.ts new file mode 100644 index 00000000000..3d00e644df7 --- /dev/null +++ b/src/items/held-items/bypass-speed-chance.ts @@ -0,0 +1,57 @@ +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { Command } from "#enums/command"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { BooleanHolder } from "#utils/common"; +import i18next from "i18next"; + +export interface BypassSpeedChanceParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Holder for whether the speed should be bypassed */ + doBypassSpeed: BooleanHolder; +} + +/** + * Modifier used for items that allow a Pokémon to bypass the speed chance (Quick Claw). + */ +export class BypassSpeedChanceHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.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} + * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied + */ + apply({ pokemon, doBypassSpeed }: BypassSpeedChanceParams): boolean { + 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..8a8c153e1a0 --- /dev/null +++ b/src/items/held-items/crit-booster.ts @@ -0,0 +1,88 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { SpeciesId } from "#enums/species-id"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; + +export interface CritBoostParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The critical hit stage */ + 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: HeldItemEffect[] = [HeldItemEffect.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: number, 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({ critStage }: CritBoostParams): boolean { + 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: number, 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: CritBoostParams): 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..1d57e61b697 --- /dev/null +++ b/src/items/held-items/damage-money-reward.ts @@ -0,0 +1,30 @@ +import { globalScene } from "#app/global-scene"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import { TrainerItemEffect } from "#items/trainer-item"; +import { NumberHolder } from "#utils/common"; + +export interface DamageMoneyRewardParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The damage, used to calculate the money reward */ + damage: number; +} + +export class DamageMoneyRewardHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.DAMAGE_MONEY_REWARD]; + + /** + * Applies {@linkcode DamageMoneyRewardModifier} + * @returns always `true` + */ + apply({ pokemon, damage }: DamageMoneyRewardParams): boolean { + const stackCount = pokemon.heldItemManager.getStack(this.type); + const moneyAmount = new NumberHolder(Math.floor(damage * (0.5 * stackCount))); + globalScene.applyPlayerItems(TrainerItemEffect.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); + globalScene.addMoney(moneyAmount.value); + + return true; + } +} diff --git a/src/items/held-items/evo-tracker.ts b/src/items/held-items/evo-tracker.ts new file mode 100644 index 00000000000..29e84958910 --- /dev/null +++ b/src/items/held-items/evo-tracker.ts @@ -0,0 +1,58 @@ +import { globalScene } from "#app/global-scene"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; +import type { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import i18next from "i18next"; + +export interface EvoTrackerParams { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +export class EvoTrackerHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.EVO_TRACKER]; + + protected species: SpeciesId; + protected required: number; + public isTransferable = false; + + constructor(type: HeldItemId, maxStackCount: number, species: SpeciesId, required: number) { + super(type, maxStackCount); + this.species = species; + this.required = required; + } + + /** + * Applies the {@linkcode EvoTrackerModifier} + * @returns always `true` + */ + apply(): boolean { + return true; + } +} + +export class GimmighoulEvoTrackerHeldItem extends EvoTrackerHeldItem { + get name(): string { + return i18next.t("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL.name"); + } + + get description(): string { + return i18next.t("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL.description"); + } + + get iconName(): string { + return "relic_gold"; + } + + getStackCount(pokemon: Pokemon): number { + const stackCount = + pokemon.heldItemManager.getStack(this.type) + + pokemon.heldItemManager.getStack(HeldItemId.GOLDEN_PUNCH) + + globalScene.trainerItems.getStack(TrainerItemId.AMULET_COIN) + + globalScene.trainerItems.getStack(TrainerItemId.GOLDEN_POKEBALL); + return stackCount; + } +} diff --git a/src/items/held-items/exp-booster.ts b/src/items/held-items/exp-booster.ts new file mode 100644 index 00000000000..e4417e8ac95 --- /dev/null +++ b/src/items/held-items/exp-booster.ts @@ -0,0 +1,53 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; +import i18next from "i18next"; + +export interface ExpBoostParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Holds the amount of experience gained, which may be modified after item application */ + expAmount: NumberHolder; +} + +export class ExpBoosterHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.EXP_BOOSTER]; + private boostPercent: number; + private boostMultiplier: number; + + constructor(type: HeldItemId, maxStackCount: number, 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} + * @returns always `true` + */ + apply({ pokemon, expAmount }: ExpBoostParams): true { + 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..25baf9c889a --- /dev/null +++ b/src/items/held-items/field-effect.ts @@ -0,0 +1,32 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; + +export interface FieldEffectParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Holder for the field effect duration*/ + 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: HeldItemEffect[] = [HeldItemEffect.FIELD_EFFECT]; + + /** + * Provides two more turns per stack to any weather or terrain effect caused + * by the holder. + * @returns always `true` + */ + apply({ pokemon, fieldDuration }: FieldEffectParams): true { + const stackCount = pokemon.heldItemManager.getStack(this.type); + 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..81320c55ba9 --- /dev/null +++ b/src/items/held-items/flinch-chance.ts @@ -0,0 +1,55 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { BooleanHolder } from "#utils/common"; + +export interface FlinchChanceParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Holds whether the attack will cause a flinch */ + 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: HeldItemEffect[] = [HeldItemEffect.FLINCH_CHANCE]; + private chance: number; + + constructor(type: HeldItemId, maxStackCount: number, 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. + * @returns `true` if {@linkcode FlinchChanceModifier} was applied successfully + */ + apply({ pokemon, flinched }: FlinchChanceParams): boolean { + 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..783b77c1a15 --- /dev/null +++ b/src/items/held-items/friendship-booster.ts @@ -0,0 +1,31 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; +import i18next from "i18next"; + +export interface FriendshipBoostParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Holder for the friendship amount to be changed by the item */ + friendship: NumberHolder; +} + +export class FriendshipBoosterHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.FRIENDSHIP_BOOSTER]; + + get description(): string { + return i18next.t("modifierType:ModifierType.PokemonFriendshipBoosterModifierType.description"); + } + + /** + * Applies {@linkcode PokemonFriendshipBoosterModifier} + * @returns always `true` + */ + apply({ pokemon, friendship }: FriendshipBoostParams): true { + 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..8e14509b429 --- /dev/null +++ b/src/items/held-items/hit-heal.ts @@ -0,0 +1,52 @@ +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; +import { toDmgValue } from "#utils/common"; +import i18next from "i18next"; + +export interface HitHealParams { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +export class HitHealHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.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} + * @returns `true` if the {@linkcode Pokemon} was healed + */ + apply({ pokemon }: HitHealParams): boolean { + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (pokemon.turnData.totalDamageDealt > 0 && !pokemon.isFullHp()) { + // TODO: this shouldn't be undefined AFAIK + globalScene.phaseManager.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..3dc62d8ebb3 --- /dev/null +++ b/src/items/held-items/incrementing-stat.ts @@ -0,0 +1,69 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import { Stat } from "#enums/stat"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; +import i18next from "i18next"; + +export interface IncrementingStatParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The stat whose value is being impacted */ + stat: Stat; + /** Holds the stat's value, which may be modified after item application */ + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2135612276 + statHolder: NumberHolder; +} + +/** + * Currently used by Macho Brace item + */ +export class IncrementingStatHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.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} + * @returns always `true` + */ + apply({ pokemon, statHolder, stat }: IncrementingStatParams): boolean { + const stackCount = pokemon.heldItemManager.getStack(this.type); + + // 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 * 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..839f660d09a --- /dev/null +++ b/src/items/held-items/instant-revive.ts @@ -0,0 +1,67 @@ +import { applyAbAttrs } from "#abilities/apply-ab-attrs"; +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { ConsumableHeldItem } from "#items/held-item"; +import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; +import { toDmgValue } from "#utils/common"; +import i18next from "i18next"; + +export interface InstantReviveParams { + /** 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: HeldItemEffect[] = [HeldItemEffect.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. + * @returns `true` if any stat stages were reset, false otherwise + */ + apply({ pokemon }: InstantReviveParams): boolean { + // 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", { pokemon: p }); + } + 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..681a1d619b1 --- /dev/null +++ b/src/items/held-items/item-steal.ts @@ -0,0 +1,169 @@ +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { allHeldItems } from "#data/data-lists"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { HeldItemId } from "#enums/held-item-id"; +import { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import { coerceArray, randSeedFloat } from "#utils/common"; +import i18next from "i18next"; + +export interface ItemStealParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The pokemon to steal from (optional) */ + // TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2135607083 + target?: Pokemon; +} + +// constructor(type: HeldItemId, maxStackCount: number, 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: ItemStealParams): 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 transferredRewards: 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)) { + transferredRewards.push(randItem); + heldItems.splice(randItemIndex, 1); + } + } + + for (const mt of transferredRewards) { + globalScene.phaseManager.queueMessage(this.getTransferMessage(params, mt)); + } + + return !!transferredRewards.length; + } + + abstract getTargets(params: ItemStealParams): Pokemon[]; + + abstract getTransferredItemCount(params: ItemStealParams): number; + + abstract getTransferMessage(params: ItemStealParams, itemId: HeldItemId): string; +} + +/** + * Modifier for held items that steal items from the enemy at the end of + * each turn. + * @see {@linkcode allRewards[MINI_BLACK_HOLE]} + */ +export class TurnEndItemStealHeldItem extends ItemTransferHeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.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: ItemStealParams): Pokemon[] { + return params.pokemon instanceof Pokemon ? params.pokemon.getOpponents() : []; + } + + getTransferredItemCount(_params: ItemStealParams): number { + return 1; + } + + getTransferMessage(params: ItemStealParams, 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 allRewards[GRIP_CLAW]} + * @see {@linkcode HeldItemTransferModifier} + */ +export class ContactItemStealChanceHeldItem extends ItemTransferHeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.CONTACT_ITEM_STEAL_CHANCE]; + public readonly chancePercent: number; + public readonly chance: number; + + constructor(type: HeldItemId, maxStackCount: number, 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({ target }: ItemStealParams): Pokemon[] { + return target ? coerceArray(target) : []; + } + + getTransferredItemCount({ pokemon }: ItemStealParams): number { + const stackCount = pokemon.heldItemManager.getStack(this.type); + return randSeedFloat() <= this.chance * stackCount ? 1 : 0; + } + + getTransferMessage({ pokemon, target }: ItemStealParams, itemId: HeldItemId): string { + return i18next.t("modifier:contactHeldItemTransferApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(target), + itemName: allHeldItems[itemId].name, + pokemonName: 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..1c2d34544e6 --- /dev/null +++ b/src/items/held-items/multi-hit.ts @@ -0,0 +1,89 @@ +import { allMoves } from "#data/data-lists"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { MoveId } from "#enums/move-id"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import { isNullOrUndefined, type NumberHolder } from "#utils/common"; +import i18next from "i18next"; + +export interface MultiHitParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** The move being used */ + moveId: MoveId; + /** Holder for the move's hit count for the turn */ + count?: NumberHolder; + /** Holder for the damage multiplier applied to a strike of the move */ + 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: HeldItemEffect[] = [HeldItemEffect.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. + * @returns Whether the item applies its effects to move + */ + apply({ pokemon, count, moveId, damageMultiplier }: MultiHitParams): 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(pokemon, 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(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..1e810798f73 --- /dev/null +++ b/src/items/held-items/nature-weight-booster.ts @@ -0,0 +1,29 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; + +export interface NatureWeightBoostParams { + /** The pokemon with the item */ + pokemon: Pokemon; + /** Holder for the multiplier */ + multiplier: NumberHolder; +} + +export class NatureWeightBoosterHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.NATURE_WEIGHT_BOOSTER]; + + /** + * Applies {@linkcode PokemonNatureWeightModifier} + * @returns `true` if multiplier was applied + */ + apply({ pokemon, multiplier }: NatureWeightBoostParams): boolean { + 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..acb935cd971 --- /dev/null +++ b/src/items/held-items/reset-negative-stat-stage.ts @@ -0,0 +1,64 @@ +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { BATTLE_STATS } from "#enums/stat"; +import type { Pokemon } from "#field/pokemon"; +import { ConsumableHeldItem } from "#items/held-item"; +import i18next from "i18next"; + +export interface ResetNegativeStatStageParams { + /** 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: HeldItemEffect[] = [HeldItemEffect.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. + * @returns `true` if any stat stages were reset, false otherwise + */ + apply({ pokemon, isPlayer }: ResetNegativeStatStageParams): 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.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..bb66e2e6d22 --- /dev/null +++ b/src/items/held-items/stat-booster.ts @@ -0,0 +1,186 @@ +import { pokemonEvolutions } from "#balance/pokemon-evolutions"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; +import type { SpeciesId } from "#enums/species-id"; +import type { Stat } from "#enums/stat"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { NumberHolder } from "#utils/common"; + +export interface StatBoostParams { + /** 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: HeldItemEffect[] = [HeldItemEffect.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: number, 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}. + * @returns `true` if the stat boost applies successfully, false otherwise + * @see shouldApply + */ + apply({ statValue }: StatBoostParams): 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 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. + * @returns `true` if the stat boost applies successfully, false otherwise + * @see shouldApply + */ + override apply(params: StatBoostParams): boolean { + const pokemon = params.pokemon; + const isUnevolved = pokemon.getSpeciesForm(true).speciesId in pokemonEvolutions; + + if (pokemon.isFusion() && pokemon.getFusionSpeciesForm(true).speciesId in pokemonEvolutions !== isUnevolved) { + // Half boost applied if pokemon is fused and either part of fusion is fully evolved + params.statValue.value *= 1 + (this.multiplier - 1) / 2; + return true; + } + if (isUnevolved && !pokemon.isMax()) { + // Full boost applied if holder is unfused and unevolved or, if fused, both parts of fusion are unevolved + return super.apply(params); + } + + return false; + } +} + +export type SpeciesStatBoosterItemId = + | typeof HeldItemId.LIGHT_BALL + | typeof HeldItemId.THICK_CLUB + | typeof HeldItemId.METAL_POWDER + | typeof HeldItemId.QUICK_POWDER + | typeof HeldItemId.DEEP_SEA_SCALE + | typeof HeldItemId.DEEP_SEA_TOOTH; + +export const SPECIES_STAT_BOOSTER_ITEMS: SpeciesStatBoosterItemId[] = [ + HeldItemId.LIGHT_BALL, + HeldItemId.THICK_CLUB, + HeldItemId.METAL_POWDER, + HeldItemId.QUICK_POWDER, + HeldItemId.DEEP_SEA_SCALE, + HeldItemId.DEEP_SEA_TOOTH, +]; + +/** + * Modifier used for held items that Applies {@linkcode Stat} boost(s) using a + * multiplier if the holder is of a specific {@linkcode SpeciesId}. + * @extends StatBoostHeldItem + * @see {@linkcode apply} + */ +export class SpeciesStatBoostHeldItem extends StatBoostHeldItem { + /** The species that the held item's stat boost(s) apply to */ + public species: SpeciesId[]; + + constructor( + type: SpeciesStatBoosterItemId, + maxStackCount: number, + 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: StatBoostParams): 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..b46297170d1 --- /dev/null +++ b/src/items/held-items/survive-chance.ts @@ -0,0 +1,54 @@ +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import type { BooleanHolder } from "#utils/common"; +import i18next from "i18next"; + +export interface SurviveChanceParams { + /** 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: HeldItemEffect[] = [HeldItemEffect.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} + * @returns `true` if the survive damage has been applied + */ + apply({ pokemon, surviveDamage }: SurviveChanceParams): boolean { + 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..f6660fab935 --- /dev/null +++ b/src/items/held-items/turn-end-heal.ts @@ -0,0 +1,36 @@ +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; +import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; +import { toDmgValue } from "#utils/common"; +import i18next from "i18next"; + +export interface TurnEndHealParams { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +export class TurnEndHealHeldItem extends HeldItem { + public effects: HeldItemEffect[] = [HeldItemEffect.TURN_END_HEAL]; + + apply({ pokemon }: TurnEndHealParams): boolean { + 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..72ccf440d4c --- /dev/null +++ b/src/items/held-items/turn-end-status.ts @@ -0,0 +1,40 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { StatusEffect } from "#enums/status-effect"; +import type { Pokemon } from "#field/pokemon"; +import { HeldItem } from "#items/held-item"; + +export interface TurnEndStatusParams { + /** 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: HeldItemEffect[] = [HeldItemEffect.TURN_END_STATUS]; + /** The status effect to be applied by the held item */ + public effect: StatusEffect; + + constructor(type: HeldItemId, maxStackCount: number, effect: StatusEffect) { + super(type, maxStackCount); + + this.effect = effect; + } + + /** + * Tries to inflicts the holder with the associated {@linkcode StatusEffect}. + * @returns `true` if the status effect was applied successfully + */ + apply({ pokemon }: TurnEndStatusParams): boolean { + return 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..3957faed1ca --- /dev/null +++ b/src/items/init-held-item-pools.ts @@ -0,0 +1,80 @@ +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { RarityTier } from "#enums/reward-tier"; +import { dailyStarterHeldItemPool, trainerHeldItemPool, wildHeldItemPool } from "#items/held-item-pool"; + +/** + * Initialize the wild held item pool + */ +function initWildHeldItemPool() { + wildHeldItemPool[RarityTier.COMMON] = [{ entry: HeldItemCategoryId.BERRY, weight: 1 }]; + wildHeldItemPool[RarityTier.GREAT] = [{ entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 1 }]; + wildHeldItemPool[RarityTier.ULTRA] = [ + { entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 5 }, + { entry: HeldItemId.WHITE_HERB, weight: 0 }, + ]; + wildHeldItemPool[RarityTier.ROGUE] = [{ entry: HeldItemId.LUCKY_EGG, weight: 4 }]; + wildHeldItemPool[RarityTier.MASTER] = [{ entry: HeldItemId.GOLDEN_EGG, weight: 1 }]; +} + +/** + * Initialize the trainer pokemon held item pool + */ +function initTrainerHeldItemPool() { + trainerHeldItemPool[RarityTier.COMMON] = [ + { entry: HeldItemCategoryId.BERRY, weight: 8 }, + { entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 3 }, + ]; + trainerHeldItemPool[RarityTier.GREAT] = [{ entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 3 }]; + trainerHeldItemPool[RarityTier.ULTRA] = [ + { entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 10 }, + { entry: HeldItemId.WHITE_HERB, weight: 0 }, + ]; + trainerHeldItemPool[RarityTier.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[RarityTier.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 initDailyStarterRewardPool() { + dailyStarterHeldItemPool[RarityTier.COMMON] = [ + { entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 1 }, + { entry: HeldItemCategoryId.BERRY, weight: 3 }, + ]; + dailyStarterHeldItemPool[RarityTier.GREAT] = [{ entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 5 }]; + dailyStarterHeldItemPool[RarityTier.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[RarityTier.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[RarityTier.MASTER] = [ + { entry: HeldItemId.LEFTOVERS, weight: 1 }, + { entry: HeldItemId.SHELL_BELL, weight: 1 }, + ]; +} + +export function initHeldItemPools() { + // Default held item pools for specific scenarios + initWildHeldItemPool(); + initTrainerHeldItemPool(); + initDailyStarterRewardPool(); +} diff --git a/src/items/init-reward-pools.ts b/src/items/init-reward-pools.ts new file mode 100644 index 00000000000..0517e4421cd --- /dev/null +++ b/src/items/init-reward-pools.ts @@ -0,0 +1,663 @@ +/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */ +import { initRewards, Reward } from "#items/reward"; +/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */ + +import { timedEventManager } from "#app/global-event-manager"; +import { globalScene } from "#app/global-scene"; +import { pokemonEvolutions } from "#balance/pokemon-evolutions"; +import { allHeldItems, allTrainerItems } from "#data/data-lists"; +import { MAX_PER_TYPE_POKEBALLS } from "#data/pokeball"; +import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; +import { MoveId } from "#enums/move-id"; +import { PokeballType } from "#enums/pokeball"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { Unlockables } from "#enums/unlockables"; +import type { Pokemon } from "#field/pokemon"; +import { rewardPool } from "#items/reward-pools"; +import type { TurnEndStatusHeldItem } from "#items/turn-end-status"; +import type { WeightedRewardWeightFunc } from "#types/rewards"; +import { isNullOrUndefined } from "#utils/common"; + +/** + * Initialize the common modifier pool + */ +function initCommonRewardPool() { + rewardPool[RarityTier.COMMON] = [ + { id: RewardId.POKEBALL, weight: () => (hasMaximumBalls(PokeballType.POKEBALL) ? 0 : 6), maxWeight: 6 }, + { id: RewardId.RARE_CANDY, weight: 2 }, + { + id: RewardId.POTION, + weight: (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter(p => p.getInverseHp() >= 10 && p.getHpRatio() <= 0.875 && !p.isFainted()).length, + 3, + ); + return thresholdPartyMemberCount * 3; + }, + maxWeight: 9, + }, + { + id: RewardId.SUPER_POTION, + weight: (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter(p => p.getInverseHp() >= 25 && p.getHpRatio() <= 0.75 && !p.isFainted()).length, + 3, + ); + return thresholdPartyMemberCount; + }, + maxWeight: 3, + }, + { + id: RewardId.ETHER, + weight: (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) && + p + .getMoveset() + .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) + .length, + ).length, + 3, + ); + return thresholdPartyMemberCount * 3; + }, + maxWeight: 9, + }, + { + id: RewardId.MAX_ETHER, + weight: (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) && + p + .getMoveset() + .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) + .length, + ).length, + 3, + ); + return thresholdPartyMemberCount; + }, + maxWeight: 3, + }, + { id: RewardId.LURE, weight: lureWeightFunc(TrainerItemId.LURE, 2) }, + { id: RewardId.TEMP_STAT_STAGE_BOOSTER, weight: 4 }, + { id: RewardId.BERRY, weight: 2 }, + { id: RewardId.TM_COMMON, weight: 2 }, + ]; +} + +/** + * Initialize the Great modifier pool + */ +function initGreatRewardPool() { + rewardPool[RarityTier.GREAT] = [ + { id: RewardId.GREAT_BALL, weight: () => (hasMaximumBalls(PokeballType.GREAT_BALL) ? 0 : 6), maxWeight: 6 }, + { id: RewardId.PP_UP, weight: 2 }, + { + id: RewardId.FULL_HEAL, + weight: (party: Pokemon[]) => { + const statusEffectPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !!p.status && + !p + .getHeldItems() + .filter(i => i in [HeldItemId.TOXIC_ORB, HeldItemId.FLAME_ORB]) + .some(i => (allHeldItems[i] as TurnEndStatusHeldItem).effect === p.status?.effect), + ).length, + 3, + ); + return statusEffectPartyMemberCount * 6; + }, + maxWeight: 18, + }, + { + id: RewardId.REVIVE, + weight: (party: Pokemon[]) => { + const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); + return faintedPartyMemberCount * 9; + }, + maxWeight: 27, + }, + { + id: RewardId.MAX_REVIVE, + weight: (party: Pokemon[]) => { + const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); + return faintedPartyMemberCount * 3; + }, + maxWeight: 9, + }, + { + id: RewardId.SACRED_ASH, + weight: (party: Pokemon[]) => { + return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; + }, + maxWeight: 1, + }, + { + id: RewardId.HYPER_POTION, + weight: (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.625 && !p.isFainted()).length, + 3, + ); + return thresholdPartyMemberCount * 3; + }, + maxWeight: 9, + }, + { + id: RewardId.MAX_POTION, + weight: (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5 && !p.isFainted()).length, + 3, + ); + return thresholdPartyMemberCount; + }, + maxWeight: 3, + }, + { + id: RewardId.FULL_RESTORE, + weight: (party: Pokemon[]) => { + const statusEffectPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !!p.status && + !p + .getHeldItems() + .filter(i => i in [HeldItemId.TOXIC_ORB, HeldItemId.FLAME_ORB]) + .some(i => (allHeldItems[i] as TurnEndStatusHeldItem).effect === p.status?.effect), + ).length, + 3, + ); + const thresholdPartyMemberCount = Math.floor( + (Math.min(party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5 && !p.isFainted()).length, 3) + + statusEffectPartyMemberCount) / + 2, + ); + return thresholdPartyMemberCount; + }, + maxWeight: 3, + }, + { + id: RewardId.ELIXIR, + weight: (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) && + p + .getMoveset() + .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) + .length, + ).length, + 3, + ); + return thresholdPartyMemberCount * 3; + }, + maxWeight: 9, + }, + { + id: RewardId.MAX_ELIXIR, + weight: (party: Pokemon[]) => { + const thresholdPartyMemberCount = Math.min( + party.filter( + p => + p.hp && + !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) && + p + .getMoveset() + .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) + .length, + ).length, + 3, + ); + return thresholdPartyMemberCount; + }, + maxWeight: 3, + }, + { id: RewardId.DIRE_HIT, weight: 4 }, + { id: RewardId.SUPER_LURE, weight: lureWeightFunc(TrainerItemId.SUPER_LURE, 4) }, + { id: RewardId.NUGGET, weight: skipInLastClassicWaveOrDefault(5) }, + { id: RewardId.SPECIES_STAT_BOOSTER, weight: 4 }, + { + id: RewardId.EVOLUTION_ITEM, + weight: () => { + return Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 15), 8); + }, + maxWeight: 8, + }, + { + id: TrainerItemId.MAP, + weight: () => (globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex < 180 ? 2 : 0), + maxWeight: 2, + }, + { id: HeldItemId.SOOTHE_BELL, weight: 2 }, + { id: RewardId.TM_GREAT, weight: 3 }, + { + id: RewardId.MEMORY_MUSHROOM, + weight: (party: Pokemon[]) => { + if (!party.find(p => p.getLearnableLevelMoves().length)) { + return 0; + } + const highestPartyLevel = party + .map(p => p.level) + .reduce((highestLevel: number, level: number) => Math.max(highestLevel, level), 1); + return Math.min(Math.ceil(highestPartyLevel / 20), 4); + }, + maxWeight: 4, + }, + { id: RewardId.BASE_STAT_BOOSTER, weight: 3 }, + { + id: RewardId.TERA_SHARD, + weight: (party: Pokemon[]) => + party.filter( + p => + !(p.hasSpecies(SpeciesId.TERAPAGOS) || p.hasSpecies(SpeciesId.OGERPON) || p.hasSpecies(SpeciesId.SHEDINJA)), + ).length > 0 + ? 1 + : 0, + }, + { + id: RewardId.DNA_SPLICERS, + weight: (party: Pokemon[]) => { + if (party.filter(p => !p.fusionSpecies).length > 1) { + if (globalScene.gameMode.isSplicedOnly) { + return 4; + } + if (globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) { + return 2; + } + } + return 0; + }, + maxWeight: 4, + }, + { + id: RewardId.VOUCHER, + weight: (_party: Pokemon[], rerollCount: number) => + !globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, + maxWeight: 1, + }, + ]; +} + +/** + * Initialize the Ultra modifier pool + */ +function initUltraRewardPool() { + rewardPool[RarityTier.ULTRA] = [ + { id: RewardId.ULTRA_BALL, weight: () => (hasMaximumBalls(PokeballType.ULTRA_BALL) ? 0 : 15), maxWeight: 15 }, + { id: RewardId.MAX_LURE, weight: lureWeightFunc(TrainerItemId.MAX_LURE, 4) }, + { id: RewardId.BIG_NUGGET, weight: skipInLastClassicWaveOrDefault(12) }, + { id: RewardId.PP_MAX, weight: 3 }, + { id: RewardId.MINT, weight: 4 }, + { + id: RewardId.RARE_EVOLUTION_ITEM, + weight: () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 15) * 4, 32), + maxWeight: 32, + }, + { + id: RewardId.FORM_CHANGE_ITEM, + weight: () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 6, + maxWeight: 24, + }, + { id: TrainerItemId.AMULET_COIN, weight: skipInLastClassicWaveOrDefault(3) }, + { + id: HeldItemId.EVIOLITE, + weight: (party: Pokemon[]) => { + const { gameMode, gameData } = globalScene; + if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) { + return party.some(p => { + // Check if Pokemon's species (or fusion species, if applicable) can evolve or if they're G-Max'd + if ( + !p.isMax() && + (p.getSpeciesForm(true).speciesId in pokemonEvolutions || + (p.isFusion() && p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)) + ) { + // Check if Pokemon is already holding an Eviolite + return !p.heldItemManager.hasItem(HeldItemId.EVIOLITE); + } + return false; + }) + ? 10 + : 0; + } + return 0; + }, + }, + { id: RewardId.RARE_SPECIES_STAT_BOOSTER, weight: 12 }, + { + id: HeldItemId.LEEK, + weight: (party: Pokemon[]) => { + const checkedSpecies = [SpeciesId.FARFETCHD, SpeciesId.GALAR_FARFETCHD, SpeciesId.SIRFETCHD]; + // 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.heldItemManager.hasItem(HeldItemId.LEEK) && + (checkedSpecies.includes(p.getSpeciesForm(true).speciesId) || + (p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId))), + ) + ? 12 + : 0; + }, + maxWeight: 12, + }, + { + id: HeldItemId.TOXIC_ORB, + weight: (party: Pokemon[]) => { + return party.some(p => { + const isHoldingOrb = p.getHeldItems().some(i => i in [HeldItemId.FLAME_ORB, HeldItemId.TOXIC_ORB]); + + if (!isHoldingOrb) { + const moveset = p + .getMoveset(true) + .filter(m => !isNullOrUndefined(m)) + .map(m => m.moveId); + const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); + + // Moves that take advantage of obtaining the actual status effect + const hasStatusMoves = [MoveId.FACADE, MoveId.PSYCHO_SHIFT].some(m => moveset.includes(m)); + // Moves that take advantage of being able to give the target a status orb + // TODO: Take moves (Trick, Fling, Switcheroo) from comment when they are implemented + const hasItemMoves = [ + /* MoveId.TRICK, MoveId.FLING, MoveId.SWITCHEROO */ + ].some(m => moveset.includes(m)); + + if (canSetStatus) { + // Abilities that take advantage of obtaining the actual status effect, separated based on specificity to the orb + const hasGeneralAbility = [ + AbilityId.QUICK_FEET, + AbilityId.GUTS, + AbilityId.MARVEL_SCALE, + AbilityId.MAGIC_GUARD, + ].some(a => p.hasAbility(a, false, true)); + const hasSpecificAbility = [AbilityId.TOXIC_BOOST, AbilityId.POISON_HEAL].some(a => + p.hasAbility(a, false, true), + ); + const hasOppositeAbility = [AbilityId.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); + + return hasSpecificAbility || (hasGeneralAbility && !hasOppositeAbility) || hasStatusMoves; + } + return hasItemMoves; + } + + return false; + }) + ? 10 + : 0; + }, + maxWeight: 10, + }, + { + id: HeldItemId.FLAME_ORB, + weight: (party: Pokemon[]) => { + return party.some(p => { + const isHoldingOrb = p.getHeldItems().some(i => i in [HeldItemId.FLAME_ORB, HeldItemId.TOXIC_ORB]); + + if (!isHoldingOrb) { + const moveset = p + .getMoveset(true) + .filter(m => !isNullOrUndefined(m)) + .map(m => m.moveId); + const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true); + + // Moves that take advantage of obtaining the actual status effect + const hasStatusMoves = [MoveId.FACADE, MoveId.PSYCHO_SHIFT].some(m => moveset.includes(m)); + // Moves that take advantage of being able to give the target a status orb + // TODO: Take moves (Trick, Fling, Switcheroo) from comment when they are implemented + const hasItemMoves = [ + /* MoveId.TRICK, MoveId.FLING, MoveId.SWITCHEROO */ + ].some(m => moveset.includes(m)); + + if (canSetStatus) { + // Abilities that take advantage of obtaining the actual status effect, separated based on specificity to the orb + const hasGeneralAbility = [ + AbilityId.QUICK_FEET, + AbilityId.GUTS, + AbilityId.MARVEL_SCALE, + AbilityId.MAGIC_GUARD, + ].some(a => p.hasAbility(a, false, true)); + const hasSpecificAbility = [AbilityId.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); + const hasOppositeAbility = [AbilityId.TOXIC_BOOST, AbilityId.POISON_HEAL].some(a => + p.hasAbility(a, false, true), + ); + + return hasSpecificAbility || (hasGeneralAbility && !hasOppositeAbility) || hasStatusMoves; + } + return hasItemMoves; + } + + return false; + }) + ? 10 + : 0; + }, + maxWeight: 10, + }, + { + id: HeldItemId.MYSTICAL_ROCK, + weight: (party: Pokemon[]) => { + return party.some(p => { + 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); + + const hasAbility = [ + AbilityId.DROUGHT, + AbilityId.ORICHALCUM_PULSE, + AbilityId.DRIZZLE, + AbilityId.SAND_STREAM, + AbilityId.SAND_SPIT, + AbilityId.SNOW_WARNING, + AbilityId.ELECTRIC_SURGE, + AbilityId.HADRON_ENGINE, + AbilityId.PSYCHIC_SURGE, + AbilityId.GRASSY_SURGE, + AbilityId.SEED_SOWER, + AbilityId.MISTY_SURGE, + ].some(a => p.hasAbility(a, false, true)); + + const hasMoves = [ + MoveId.SUNNY_DAY, + MoveId.RAIN_DANCE, + MoveId.SANDSTORM, + MoveId.SNOWSCAPE, + MoveId.HAIL, + MoveId.CHILLY_RECEPTION, + MoveId.ELECTRIC_TERRAIN, + MoveId.PSYCHIC_TERRAIN, + MoveId.GRASSY_TERRAIN, + MoveId.MISTY_TERRAIN, + ].some(m => moveset.includes(m)); + + return hasAbility || hasMoves; + } + return false; + }) + ? 10 + : 0; + }, + maxWeight: 10, + }, + { id: HeldItemId.REVIVER_SEED, weight: 4 }, + { id: TrainerItemId.CANDY_JAR, weight: skipInLastClassicWaveOrDefault(5) }, + { id: RewardId.ATTACK_TYPE_BOOSTER, weight: 9 }, + { id: RewardId.TM_ULTRA, weight: 11 }, + { id: RewardId.RARER_CANDY, weight: 4 }, + { id: HeldItemId.GOLDEN_PUNCH, weight: skipInLastClassicWaveOrDefault(2) }, + { id: TrainerItemId.IV_SCANNER, weight: skipInLastClassicWaveOrDefault(4) }, + { id: TrainerItemId.EXP_CHARM, weight: skipInLastClassicWaveOrDefault(8) }, + { id: TrainerItemId.EXP_SHARE, weight: skipInLastClassicWaveOrDefault(10) }, + { + id: TrainerItemId.TERA_ORB, + weight: () => + !globalScene.gameMode.isClassic + ? Math.min(Math.max(Math.floor(globalScene.currentBattle.waveIndex / 50) * 2, 1), 4) + : 0, + maxWeight: 4, + }, + { id: HeldItemId.QUICK_CLAW, weight: 3 }, + { id: HeldItemId.WIDE_LENS, weight: 7 }, + ]; +} + +function initRogueRewardPool() { + rewardPool[RarityTier.ROGUE] = [ + { id: RewardId.ROGUE_BALL, weight: () => (hasMaximumBalls(PokeballType.ROGUE_BALL) ? 0 : 16), maxWeight: 16 }, + { id: RewardId.RELIC_GOLD, weight: skipInLastClassicWaveOrDefault(2) }, + { id: HeldItemId.LEFTOVERS, weight: 3 }, + { id: HeldItemId.SHELL_BELL, weight: 3 }, + { id: TrainerItemId.BERRY_POUCH, weight: 4 }, + { id: HeldItemId.GRIP_CLAW, weight: 5 }, + { id: HeldItemId.SCOPE_LENS, weight: 4 }, + { id: HeldItemId.BATON, weight: 2 }, + { id: HeldItemId.SOUL_DEW, weight: 7 }, + { id: TrainerItemId.CATCHING_CHARM, weight: () => (!globalScene.gameMode.isClassic ? 4 : 0), maxWeight: 4 }, + { id: TrainerItemId.ABILITY_CHARM, weight: skipInClassicAfterWave(189, 6) }, + { id: HeldItemId.FOCUS_BAND, weight: 5 }, + { id: HeldItemId.KINGS_ROCK, weight: 3 }, + { id: TrainerItemId.LOCK_CAPSULE, weight: () => (globalScene.gameMode.isClassic ? 0 : 3) }, + { id: TrainerItemId.SUPER_EXP_CHARM, weight: skipInLastClassicWaveOrDefault(8) }, + { + id: RewardId.RARE_FORM_CHANGE_ITEM, + weight: () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 6, + maxWeight: 24, + }, + { + id: TrainerItemId.MEGA_BRACELET, + weight: () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 9, + maxWeight: 36, + }, + { + id: TrainerItemId.DYNAMAX_BAND, + weight: () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 9, + maxWeight: 36, + }, + { + id: RewardId.VOUCHER_PLUS, + weight: (_party: Pokemon[], rerollCount: number) => + !globalScene.gameMode.isDaily ? Math.max(3 - rerollCount * 1, 0) : 0, + maxWeight: 3, + }, + ]; +} + +/** + * Initialize the Master modifier pool + */ +function initMasterRewardPool() { + rewardPool[RarityTier.MASTER] = [ + { id: RewardId.MASTER_BALL, weight: () => (hasMaximumBalls(PokeballType.MASTER_BALL) ? 0 : 24), maxWeight: 24 }, + { id: TrainerItemId.SHINY_CHARM, weight: 14 }, + { id: TrainerItemId.HEALING_CHARM, weight: 18 }, + { id: HeldItemId.MULTI_LENS, weight: 18 }, + { + id: RewardId.VOUCHER_PREMIUM, + weight: (_party: Pokemon[], rerollCount: number) => + !globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly + ? Math.max(5 - rerollCount * 2, 0) + : 0, + maxWeight: 5, + }, + { + id: RewardId.DNA_SPLICERS, + weight: (party: Pokemon[]) => + !(globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) && + !globalScene.gameMode.isSplicedOnly && + party.filter(p => !p.fusionSpecies).length > 1 + ? 24 + : 0, + maxWeight: 24, + }, + { + id: HeldItemId.MINI_BLACK_HOLE, + weight: () => + globalScene.gameMode.isDaily || + (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE)) + ? 1 + : 0, + maxWeight: 1, + }, + ]; +} + +/** + * Initialize {@linkcode rewardPool} with the initial set of modifier types. + * {@linkcode initRewards} MUST be called before this function. + */ +export function initRewardPools() { + // The modifier pools the player chooses from during modifier selection + initCommonRewardPool(); + initGreatRewardPool(); + initUltraRewardPool(); + initRogueRewardPool(); + initMasterRewardPool(); +} + +/** + * High order function that returns a WeightedRewardWeightFunc that will only be applied on + * classic and skip an Reward if current wave is greater or equal to the one passed down + * @param wave - Wave where we should stop showing the modifier + * @param defaultWeight - Reward default weight + * @returns A WeightedRewardWeightFunc + */ +function skipInClassicAfterWave(wave: number, defaultWeight: number): WeightedRewardWeightFunc { + return () => { + const gameMode = globalScene.gameMode; + const currentWave = globalScene.currentBattle.waveIndex; + return gameMode.isClassic && currentWave >= wave ? 0 : defaultWeight; + }; +} + +/** + * High order function that returns a WeightedRewardWeightFunc that will only be applied on + * classic and it will skip a Reward if it is the last wave pull. + * @param defaultWeight Reward default weight + * @returns A WeightedRewardWeightFunc + */ +function skipInLastClassicWaveOrDefault(defaultWeight: number): WeightedRewardWeightFunc { + return skipInClassicAfterWave(199, defaultWeight); +} + +/** + * High order function that returns a WeightedRewardWeightFunc to ensure Lures don't spawn on Classic 199 + * or if the lure still has over 60% of its duration left + * @param lureId The id of the lure type in question. + * @param weight The desired weight for the lure when it does spawn + * @returns A WeightedRewardWeightFunc + */ +function lureWeightFunc(lureId: TrainerItemId, weight: number): WeightedRewardWeightFunc { + return () => { + const lureCount = globalScene.trainerItems.getStack(lureId); + return !(globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex === 199) && + lureCount < allTrainerItems[lureId].getMaxStackCount() * 0.6 + ? weight + : 0; + }; +} + +/** + * Used to check if the player has max of a given ball type in Classic + * @param ballType The {@linkcode PokeballType} being checked + * @returns boolean: true if the player has the maximum of a given ball type + */ +function hasMaximumBalls(ballType: PokeballType): boolean { + return globalScene.gameMode.isClassic && globalScene.pokeballCounts[ballType] >= MAX_PER_TYPE_POKEBALLS; +} diff --git a/src/items/init-trainer-item-pools.ts b/src/items/init-trainer-item-pools.ts new file mode 100644 index 00000000000..3b8de62f0bc --- /dev/null +++ b/src/items/init-trainer-item-pools.ts @@ -0,0 +1,41 @@ +import { RarityTier } from "#enums/reward-tier"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { enemyBuffTokenPool } from "#items/trainer-item-pool"; + +/** + * Initialize the enemy buff modifier pool + */ +function initEnemyBuffTokenPool() { + enemyBuffTokenPool[RarityTier.COMMON] = [ + { entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 9 }, + { entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 9 }, + { entry: TrainerItemId.ENEMY_ATTACK_POISON_CHANCE, weight: 3 }, + { entry: TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE, weight: 3 }, + { entry: TrainerItemId.ENEMY_ATTACK_BURN_CHANCE, weight: 3 }, + { entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 9 }, + { entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 4 }, + { entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 1 }, + ]; + enemyBuffTokenPool[RarityTier.GREAT] = [ + { entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 5 }, + { entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 5 }, + { entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 5 }, + { entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 5 }, + { entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 1 }, + ]; + enemyBuffTokenPool[RarityTier.ULTRA] = [ + { entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 10 }, + { entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 10 }, + { entry: TrainerItemId.ENEMY_HEAL, weight: 10 }, + { entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 10 }, + { entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 10 }, + { entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 5 }, + ]; + enemyBuffTokenPool[RarityTier.ROGUE] = []; + enemyBuffTokenPool[RarityTier.MASTER] = []; +} + +export function initTrainerItemPools() { + // Default held item pools for specific scenarios + initEnemyBuffTokenPool(); +} diff --git a/src/items/item-overrides.ts b/src/items/item-overrides.ts new file mode 100644 index 00000000000..8e179d618fa --- /dev/null +++ b/src/items/item-overrides.ts @@ -0,0 +1,50 @@ +import { globalScene } from "#app/global-scene"; +import Overrides from "#app/overrides"; +import type { Pokemon } from "#field/pokemon"; +import type { HeldItemConfiguration } from "#items/held-item-data-types"; +import { assignItemsFromConfiguration } from "#items/held-item-pool"; +import type { TrainerItemConfiguration } from "#items/trainer-item-data-types"; + +/** + * Uses either `MODIFIER_OVERRIDE` in overrides.ts to set {@linkcode PersistentModifier}s for either: + * - The player + * - The enemy + * @param isPlayer {@linkcode boolean} for whether the player (`true`) or enemy (`false`) is being overridden + */ +export function overrideTrainerItems(isPlayer = true): void { + const trainerItemsOverride: TrainerItemConfiguration = isPlayer + ? Overrides.STARTING_TRAINER_ITEMS_OVERRIDE + : Overrides.OPP_TRAINER_ITEMS_OVERRIDE; + if (!trainerItemsOverride || trainerItemsOverride.length === 0 || !globalScene) { + return; + } + + // If it's the opponent, clear all of their current modifiers to avoid stacking + if (!isPlayer) { + globalScene.clearEnemyItems(); + } + + globalScene.assignTrainerItemsFromConfiguration(trainerItemsOverride, isPlayer); +} + +/** + * Uses either `HELD_ITEMS_OVERRIDE` in overrides.ts to set {@linkcode PokemonHeldItemModifier}s for either: + * - The first member of the player's team when starting a new game + * - An enemy {@linkcode Pokemon} being spawned in + * @param pokemon {@linkcode Pokemon} whose held items are being overridden + * @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: HeldItemConfiguration = isPlayer + ? Overrides.STARTING_HELD_ITEMS_OVERRIDE + : Overrides.OPP_HELD_ITEMS_OVERRIDE; + if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) { + return; + } + + if (!isPlayer) { + pokemon.heldItemManager.clearItems(); + } + + assignItemsFromConfiguration(heldItemsOverride, pokemon); +} diff --git a/src/items/item-utility.ts b/src/items/item-utility.ts new file mode 100644 index 00000000000..4108ab7e636 --- /dev/null +++ b/src/items/item-utility.ts @@ -0,0 +1,72 @@ +import { globalScene } from "#app/global-scene"; +import { allHeldItems, allTrainerItems } from "#data/data-lists"; +import { FormChangeItem } from "#enums/form-change-item"; +import { HeldItemCategoryId, type HeldItemId, isItemInCategory } from "#enums/held-item-id"; +import type { TrainerItemId } from "#enums/trainer-item-id"; +import type { Pokemon } from "#field/pokemon"; +import i18next from "i18next"; +import type { PokemonItemMap } from "./held-item-data-types"; + +export function formChangeItemName(id: FormChangeItem) { + return i18next.t(`modifierType:FormChangeItem.${FormChangeItem[id]}`); +} + +export const trainerItemSortFunc = (a: TrainerItemId, b: TrainerItemId): number => { + const itemNameMatch = allTrainerItems[a].name.localeCompare(allTrainerItems[b].name); + const itemIdMatch = a - b; + + if (itemIdMatch === 0) { + return itemNameMatch; + //Finally sort by item name + } + return itemIdMatch; +}; + +//TODO: revisit this function +export const heldItemSortFunc = (a: HeldItemId, b: HeldItemId): number => { + const itemNameMatch = allHeldItems[a].name.localeCompare(allHeldItems[b].name); + const itemIdMatch = a - b; + + if (itemIdMatch === 0) { + return itemNameMatch; + //Finally sort by item name + } + return itemIdMatch; +}; + +export const formChangeItemSortFunc = (a: FormChangeItem, b: FormChangeItem): number => { + const nameA = formChangeItemName(a); + const nameB = formChangeItemName(b); + const itemNameMatch = nameA.localeCompare(nameB); + const itemIdMatch = a - b; + + if (itemIdMatch === 0) { + return itemNameMatch; + //Finally sort by item name + } + return itemIdMatch; +}; + +// 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); + pokemonItems.push({ item: { id: berryId, stack: berryStack }, pokemonId: pokemon.id }); + }); + }); + return pokemonItems; +} diff --git a/src/items/modifier-to-item-migrator-utils.ts b/src/items/modifier-to-item-migrator-utils.ts new file mode 100644 index 00000000000..d19fb52cfc6 --- /dev/null +++ b/src/items/modifier-to-item-migrator-utils.ts @@ -0,0 +1,152 @@ +import type { 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 { type PermanentStat, Stat } from "#enums/stat"; +import { attackTypeToHeldItem } from "#items/attack-type-booster"; +import { permanentStatToHeldItem } from "#items/base-stat-booster"; +import { berryTypeToHeldItem } from "#items/berry"; +import type { PokemonItemMap } from "#items/held-item-data-types"; + +const uniqueModifierToItem = { + EvoTrackerModifier: HeldItemId.GIMMIGHOUL_EVO_TRACKER, + PokemonBaseStatFlatModifier: HeldItemId.OLD_GATEAU, + PokemonIncrementingStatModifier: HeldItemId.MACHO_BRACE, + SurviveDamageModifier: HeldItemId.FOCUS_BAND, + BypassSpeedChanceModifier: HeldItemId.QUICK_CLAW, + FlinchChanceModifier: HeldItemId.KINGS_ROCK, + TurnHealModifier: HeldItemId.LEFTOVERS, + HitHealModifier: HeldItemId.SHELL_BELL, + PokemonInstantReviveModifier: HeldItemId.REVIVER_SEED, + ResetNegativeStatStageModifier: HeldItemId.WHITE_HERB, + FieldEffectModifier: HeldItemId.MYSTICAL_ROCK, + PokemonFriendshipBoosterModifier: HeldItemId.SOOTHE_BELL, + PokemonNatureWeightModifier: HeldItemId.SOUL_DEW, + PokemonMoveAccuracyBoosterModifier: HeldItemId.WIDE_LENS, + PokemonMultiHitModifier: HeldItemId.MULTI_LENS, + DamageMoneyRewardModifier: HeldItemId.GOLDEN_PUNCH, + SwitchEffectTransferModifier: HeldItemId.BATON, + TurnHeldItemTransferModifier: HeldItemId.MINI_BLACK_HOLE, + ContactHeldItemTransferChanceModifier: HeldItemId.GRIP_CLAW, +} as const; + +type UniqueModifierString = keyof typeof uniqueModifierToItem; + +function isUniqueModifierString(value: string): value is UniqueModifierString { + return value in uniqueModifierToItem; +} + +const modifierCategoryList = [ + "BaseStatModifier", + "EvolutionStatBoosterModifier", + "SpeciesStatBoosterModifier", + "CritBoosterModifier", + "SpeciesCritBoosterModifier", + "AttackTypeBoosterModifier", + "TurnStatusEffectModifier", + "BerryModifier", + "PokemonExpBoosterModifier", + "PokemonFormChangeItemModifier", + "PokemonBaseStatTotalModifier", +] as const; + +type ModifierCategoryString = (typeof modifierCategoryList)[number]; + +function isModifierCategoryString(value: string): value is ModifierCategoryString { + return modifierCategoryList.includes(value as ModifierCategoryString); +} + +function mapModifierCategoryToItems(modifier: ModifierCategoryString, typeId: string, args: any): HeldItemId { + if (modifier === "BaseStatModifier") { + const stat = args[1] as PermanentStat; + return permanentStatToHeldItem[stat]; + } + if (modifier === "EvolutionStatBoosterModifier") { + return HeldItemId.EVIOLITE; + } + if (modifier === "SpeciesStatBoosterModifier") { + const stats = args[1]; + const species = args[3]; + // TODO: why is this not `species === SpeciesId.SPECIES_NAME`? + if (SpeciesId.PIKACHU in species) { + return HeldItemId.LIGHT_BALL; + } + if (SpeciesId.CUBONE in species) { + return HeldItemId.THICK_CLUB; + } + if (SpeciesId.DITTO in species && Stat.DEF in stats) { + return HeldItemId.METAL_POWDER; + } + if (SpeciesId.DITTO in species && Stat.SPDEF in stats) { + return HeldItemId.QUICK_POWDER; + } + if (SpeciesId.CLAMPERL in species && Stat.SPDEF in stats) { + return HeldItemId.DEEP_SEA_SCALE; + } + if (SpeciesId.CLAMPERL in species && Stat.SPATK in stats) { + return HeldItemId.DEEP_SEA_TOOTH; + } + } + if (modifier === "CritBoosterModifier") { + return HeldItemId.SCOPE_LENS; + } + if (modifier === "SpeciesCritBoosterModifier") { + return HeldItemId.LEEK; + } + if (modifier === "AttackTypeBoosterModifier") { + const moveType = args[1] as PokemonType; + return attackTypeToHeldItem[moveType]; + } + if (modifier === "TurnStatusEffectModifier") { + switch (typeId) { + case "TOXIC_ORB": + return HeldItemId.TOXIC_ORB; + case "FLAME_ORB": + return HeldItemId.FLAME_ORB; + } + } + if (modifier === "BerryModifier") { + const berryType = args[1] as BerryType; + return berryTypeToHeldItem[berryType]; + } + if (modifier === "PokemonExpBoosterModifier") { + const boost = args[1] as number; + return boost === 100 ? HeldItemId.GOLDEN_EGG : HeldItemId.LUCKY_EGG; + } + if (modifier === "PokemonBaseStatTotalModifier") { + const statModifier = args[1] as number; + return statModifier > 0 ? HeldItemId.SHUCKLE_JUICE_GOOD : HeldItemId.SHUCKLE_JUICE_BAD; + } + return 0; +} + +export function convertModifierSaveData(data: ModifierData[]) { + const pokemonItems: PokemonItemMap[] = []; + for (const entry of data) { + const typeId = entry.typeId; + const args = entry.args; + const pokemonId = args[0]; + const stack = entry.stackCount; + const className = entry.className; + + if (className === "PokemonFormChangeItemModifier") { + } + //TODO: Code to filter out modifiers which are not held items + + let itemId: HeldItemId = 0; + + if (isModifierCategoryString(className)) { + itemId = mapModifierCategoryToItems(className, typeId, args); + } + + if (isUniqueModifierString(className)) { + itemId = uniqueModifierToItem[className]; + } + + if (itemId) { + const specs = { id: itemId, stack: stack }; + const pokemonItem = { item: specs, pokemonId: pokemonId }; + pokemonItems.push(pokemonItem); + } + } +} diff --git a/src/items/reward-defaults-tiers.ts b/src/items/reward-defaults-tiers.ts new file mode 100644 index 00000000000..2c720d97405 --- /dev/null +++ b/src/items/reward-defaults-tiers.ts @@ -0,0 +1,72 @@ +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; + +export const rewardRarities = { + [RewardId.POKEBALL]: RarityTier.COMMON, + [RewardId.GREAT_BALL]: RarityTier.GREAT, + [RewardId.ULTRA_BALL]: RarityTier.ULTRA, + [RewardId.ROGUE_BALL]: RarityTier.ROGUE, + [RewardId.MASTER_BALL]: RarityTier.MASTER, + + [RewardId.VOUCHER]: RarityTier.GREAT, + [RewardId.VOUCHER_PLUS]: RarityTier.ROGUE, + [RewardId.VOUCHER_PREMIUM]: RarityTier.MASTER, + + [RewardId.NUGGET]: RarityTier.GREAT, + [RewardId.BIG_NUGGET]: RarityTier.ULTRA, + [RewardId.RELIC_GOLD]: RarityTier.ROGUE, + + [RewardId.RARE_CANDY]: RarityTier.COMMON, + [RewardId.RARER_CANDY]: RarityTier.ULTRA, + + [RewardId.EVOLUTION_ITEM]: RarityTier.GREAT, + [RewardId.RARE_EVOLUTION_ITEM]: RarityTier.ULTRA, + + [RewardId.POTION]: RarityTier.COMMON, + [RewardId.SUPER_POTION]: RarityTier.COMMON, + [RewardId.HYPER_POTION]: RarityTier.GREAT, + [RewardId.MAX_POTION]: RarityTier.GREAT, + [RewardId.FULL_HEAL]: RarityTier.GREAT, + [RewardId.FULL_RESTORE]: RarityTier.GREAT, + + [RewardId.REVIVE]: RarityTier.GREAT, + [RewardId.MAX_REVIVE]: RarityTier.GREAT, + [RewardId.SACRED_ASH]: RarityTier.GREAT, + + [RewardId.ETHER]: RarityTier.COMMON, + [RewardId.MAX_ETHER]: RarityTier.COMMON, + + [RewardId.ELIXIR]: RarityTier.GREAT, + [RewardId.MAX_ELIXIR]: RarityTier.GREAT, + + [RewardId.PP_UP]: RarityTier.GREAT, + [RewardId.PP_MAX]: RarityTier.ULTRA, + + [RewardId.TM_COMMON]: RarityTier.COMMON, + [RewardId.TM_GREAT]: RarityTier.GREAT, + [RewardId.TM_ULTRA]: RarityTier.ULTRA, + + [RewardId.MINT]: RarityTier.ULTRA, + [RewardId.TERA_SHARD]: RarityTier.GREAT, + [RewardId.MEMORY_MUSHROOM]: RarityTier.GREAT, + [RewardId.DNA_SPLICERS]: RarityTier.MASTER, + + [RewardId.SPECIES_STAT_BOOSTER]: RarityTier.GREAT, + [RewardId.RARE_SPECIES_STAT_BOOSTER]: RarityTier.ULTRA, + [RewardId.BASE_STAT_BOOSTER]: RarityTier.GREAT, + [RewardId.ATTACK_TYPE_BOOSTER]: RarityTier.ULTRA, + [RewardId.BERRY]: RarityTier.COMMON, + + [RewardId.TEMP_STAT_STAGE_BOOSTER]: RarityTier.COMMON, + [RewardId.LURE]: RarityTier.COMMON, + [RewardId.SUPER_LURE]: RarityTier.GREAT, + [RewardId.MAX_LURE]: RarityTier.ULTRA, + + [RewardId.FORM_CHANGE_ITEM]: RarityTier.ULTRA, + [RewardId.RARE_FORM_CHANGE_ITEM]: RarityTier.ROGUE, +}; + +export function getRewardTier(reward: RewardId): RarityTier { + const tier = rewardRarities[reward]; + return tier ?? RarityTier.LUXURY; +} diff --git a/src/items/reward-pool-utils.ts b/src/items/reward-pool-utils.ts new file mode 100644 index 00000000000..4ad5df55b9a --- /dev/null +++ b/src/items/reward-pool-utils.ts @@ -0,0 +1,315 @@ +import { globalScene } from "#app/global-scene"; +import Overrides from "#app/overrides"; +import { RewardPoolType } from "#enums/reward-pool-type"; +import { RarityTier } from "#enums/reward-tier"; +import type { PlayerPokemon, Pokemon } from "#field/pokemon"; +import type { RewardPool, RewardPoolWeights, RewardSpecs } from "#types/rewards"; +import { isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#utils/common"; +import { getPartyLuckValue } from "#utils/party"; +import type { RewardOption } from "./reward"; +import { rewardPool, rewardPoolWeights } from "./reward-pools"; +import { generateRewardOptionFromId, generateRewardOptionFromSpecs, isTrainerItemId } from "./reward-utils"; + +/* +This file still contains several functions to generate rewards from pools. The hierarchy of these functions is explained here. + +At the top of the food chain is `generatePlayerRewardOptions`, which is responsible for creating item rewards for the player. +It can take a `CustomRewardSettings` to fix any number of rewards or tiers, then fills the remaining spots randomly. +Note that this function generates `RewardOption` instances, not yet `Reward`s. +Currently, there is only one reward pool, but in the future we will want to allow for custom pools. + +The function `getNewRewardOption` is responsible for generating a single RewardOption from a given pool and set of weights. +Note that, in the previous system, this function could in principle generate rewards for enemies, which was used in some +cases to assign modifiers. This usage is now deprecated, as we have separate pools for held items and trainer items for enemies. + +However, `getNewRewardOption` is not called directly by `generatePlayerRewardOptions`. Instead, it is filtered +by `getRewardOptionWithRetry`, which also checks existing rewards to minimize the chance of duplicates. + +This will allow more customization in creating pools for challenges, MEs etc. +*/ + +export interface CustomRewardSettings { + guaranteedRarityTiers?: RarityTier[]; + guaranteedRewardOptions?: RewardOption[]; + /** If specified, will override the next X items to be auto-generated from specific reward functions (these don't have to be pre-genned). */ + guaranteedRewardSpecs?: RewardSpecs[]; + /** + * 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 + * ```ts + * count = 4; + * customRewardSettings = { guaranteedRarityTiers: [RarityTier.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 the value of `count`). + * @defaultValue `false` + */ + fillRemaining?: boolean; + /** 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. */ + rerollMultiplier?: number; + /** + * If `false`, will prevent set item tiers from upgrading via luck. + * @defaultValue `true` + */ + allowLuckUpgrades?: boolean; +} + +/** + * Generates weights for a {@linkcode RewardPool}. An array of weights is generated for each rarity tier. Weights can be 0. + * @param pool - The pool for which weights must be generated + * @param party - Party is required for generating the weights + * @param rerollCount - (Optional) Needed for weights of vouchers. + */ +export function generateRewardPoolWeights(pool: RewardPool, party: Pokemon[], rerollCount = 0) { + for (const tier of Object.keys(pool)) { + const poolWeights = pool[tier].map(w => { + if (isTrainerItemId(w.id)) { + if (globalScene.trainerItems.isMaxStack(w.id)) { + return 0; + } + } + if (typeof w.weight === "number") { + return w.weight; + } + return w.weight(party, rerollCount); + }); + rewardPoolWeights[tier] = poolWeights; + } +} + +/** + * Generates a random RarityTier to draw rewards from the pool. The probabilities are: + * 1/1024 (Master tier) + * 12/1024 (Rogue tier) + * 48/1024 (Ultra tier) + * 195/1024 (Great tier) + * 768/1024 (Common tier) + * return {@linkcode RarityTier} + */ +function randomBaseTier(): RarityTier { + const tierValue = randSeedInt(1024); + + if (tierValue > 255) { + return RarityTier.COMMON; + } + if (tierValue > 60) { + return RarityTier.GREAT; + } + if (tierValue > 12) { + return RarityTier.ULTRA; + } + if (tierValue) { + return RarityTier.ROGUE; + } + return RarityTier.MASTER; +} + +/** + * Determines the upgrade count for a given rarity tier, based on the party luck. Will not update + * if the pool would have no entries at the new rarity. + * @param pool - RewardPool from which the reward will be generated + * @param baseTier - The initial tier to upgrade + * @param party - Party of the trainer using the item + * return {@linkcode RarityTier} + */ +function getRarityUpgradeCount(pool: RewardPool, baseTier: RarityTier, party: Pokemon[]): RarityTier { + let upgradeCount = 0; + if (baseTier < RarityTier.MASTER) { + const partyLuckValue = getPartyLuckValue(party); + const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4)); + while (pool.hasOwnProperty(baseTier + upgradeCount + 1) && pool[baseTier + upgradeCount + 1].length) { + if (randSeedInt(upgradeOdds) < 4) { + upgradeCount++; + } else { + break; + } + } + } + return upgradeCount; +} + +/** + * Generates reward options for a {@linkcode SelectRewardPhase} + * @param count - Determines the number of items to generate + * @param party - Party is required for generating proper reward pools + * @param rarityTiers - (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule. + * @param customRewardSettings - (Optional) See {@linkcode CustomRewardSettings} + */ +export function generatePlayerRewardOptions( + count: number, + party: PlayerPokemon[], + rarityTiers?: RarityTier[], + customRewardSettings?: CustomRewardSettings, +): RewardOption[] { + const options: RewardOption[] = []; + const retryCount = Math.min(count * 5, 50); + // TODO: Change this to allow for custom reward pools + const pool = getRewardPoolForType(RewardPoolType.PLAYER); + const weights = getRewardWeightsForType(RewardPoolType.PLAYER); + if (!customRewardSettings) { + for (let i = 0; i < count; i++) { + const tier = rarityTiers && rarityTiers.length > i ? rarityTiers[i] : undefined; + options.push(getRewardOptionWithRetry(pool, weights, options, retryCount, party, tier)); + } + } else { + // Guaranteed reward options first + if (customRewardSettings?.guaranteedRewardOptions && customRewardSettings.guaranteedRewardOptions.length > 0) { + options.push(...customRewardSettings.guaranteedRewardOptions!); + } + + if (customRewardSettings?.guaranteedRewardSpecs && customRewardSettings.guaranteedRewardSpecs.length > 0) { + for (const specs of customRewardSettings.guaranteedRewardSpecs) { + const rewardOption = generateRewardOptionFromSpecs(specs); + if (rewardOption) { + options.push(rewardOption); + } + } + } + + // Guaranteed tiers third + if (customRewardSettings.guaranteedRarityTiers && customRewardSettings.guaranteedRarityTiers.length > 0) { + const allowLuckUpgrades = customRewardSettings.allowLuckUpgrades ?? true; + for (const tier of customRewardSettings.guaranteedRarityTiers) { + options.push(getRewardOptionWithRetry(pool, weights, options, retryCount, party, tier, allowLuckUpgrades)); + } + } + + // Fill remaining + if (options.length < count && customRewardSettings.fillRemaining) { + while (options.length < count) { + options.push(getRewardOptionWithRetry(pool, weights, options, retryCount, party, undefined)); + } + } + } + + // Applies overrides for testing + overridePlayerRewardOptions(options, party); + + return options; +} + +/** + * Will generate a {@linkcode RewardOption} from the {@linkcode RewardPoolType.PLAYER} pool, attempting to retry duplicated items up to retryCount + * @param pool - {@linkcode RewardPool} to generate items from + * @param weights - {@linkcode RewardPoolWeights} to use when generating items + * @param existingOptions Currently generated options + * @param retryCount How many times to retry before allowing a dupe item + * @param party Current player party, used to calculate items in the pool + * @param tier If specified will generate item of tier + * @param allowLuckUpgrades `true` to allow items to upgrade tiers (the little animation that plays and is affected by luck) + */ +function getRewardOptionWithRetry( + pool: RewardPool, + weights: RewardPoolWeights, + existingOptions: RewardOption[], + retryCount: number, + party: PlayerPokemon[], + tier?: RarityTier, + allowLuckUpgrades?: boolean, +): RewardOption { + allowLuckUpgrades = allowLuckUpgrades ?? true; + let candidate = getNewRewardOption(pool, weights, party, tier, undefined, 0, allowLuckUpgrades); + let r = 0; + while ( + existingOptions.length && + ++r < retryCount && + //TODO: Improve this condition to refine what counts as a dupe + 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 = getNewRewardOption( + pool, + weights, + party, + candidate?.type.tier ?? tier, + candidate?.upgradeCount, + 0, + allowLuckUpgrades, + ); + } + return candidate!; +} + +/** + * Generates a Reward from the specified pool + * @param pool - {@linkcode RewardPool} to generate items from + * @param weights - {@linkcode RewardPoolWeights} to use when generating items + * @param party - party of the trainer using the item + * @param baseTier - If specified, will override the initial tier of an item (can still upgrade with luck) + * @param upgradeCount - If defined, means that this is a new Reward being generated to override another via luck upgrade. Used for recursive logic + * @param retryCount - Max allowed tries before the next tier down is checked for a valid Reward + * @param allowLuckUpgrades - Default true. If false, will not allow Reward to randomly upgrade to next tier + */ +function getNewRewardOption( + pool: RewardPool, + weights: RewardPoolWeights, + party: PlayerPokemon[], + baseTier?: RarityTier, + upgradeCount?: number, + retryCount = 0, + allowLuckUpgrades = true, +): RewardOption | null { + let tier = 0; + if (isNullOrUndefined(baseTier)) { + baseTier = randomBaseTier(); + } + if (isNullOrUndefined(upgradeCount)) { + upgradeCount = allowLuckUpgrades ? getRarityUpgradeCount(pool, baseTier, party) : 0; + tier = baseTier + upgradeCount; + } else { + tier = baseTier; + } + + const tierWeights = weights[tier]; + const index = pickWeightedIndex(tierWeights); + + if (index === undefined) { + return null; + } + + const rewardOption = generateRewardOptionFromId(pool[tier][index].id, 0, tier, upgradeCount); + if (rewardOption === null) { + console.log(RarityTier[tier], upgradeCount); + return getNewRewardOption(pool, weights, party, tier, upgradeCount, ++retryCount); + } + + console.log(rewardOption); + + return rewardOption; +} + +/** + * Replaces the {@linkcode Reward} of the entries within {@linkcode options} with any + * up to the smallest amount of entries between {@linkcode options} and the override array. + * @param options Array of naturally rolled {@linkcode RewardOption}s + * @param party Array of the player's current party + */ +export function overridePlayerRewardOptions(options: RewardOption[]) { + const minLength = Math.min(options.length, Overrides.REWARD_OVERRIDE.length); + for (let i = 0; i < minLength; i++) { + const specs: RewardSpecs = Overrides.REWARD_OVERRIDE[i]; + const rewardOption = generateRewardOptionFromSpecs(specs); + if (rewardOption) { + options[i] = rewardOption; + } + } +} + +export function getRewardPoolForType(poolType: RewardPoolType): RewardPool { + switch (poolType) { + case RewardPoolType.PLAYER: + return rewardPool; + } +} + +export function getRewardWeightsForType(poolType: RewardPoolType): RewardPoolWeights { + switch (poolType) { + case RewardPoolType.PLAYER: + return rewardPoolWeights; + } +} diff --git a/src/items/reward-pools.ts b/src/items/reward-pools.ts new file mode 100644 index 00000000000..12affd1c5df --- /dev/null +++ b/src/items/reward-pools.ts @@ -0,0 +1,9 @@ +/* + * Contains modifier pools for different contexts in the game. + * Can be safely imported without worrying about circular dependencies. + */ + +import type { RewardPool, RewardPoolWeights } from "#types/rewards"; + +export const rewardPool: RewardPool = {}; +export const rewardPoolWeights: RewardPoolWeights = {}; diff --git a/src/items/reward-utils.ts b/src/items/reward-utils.ts new file mode 100644 index 00000000000..7f229b65c94 --- /dev/null +++ b/src/items/reward-utils.ts @@ -0,0 +1,125 @@ +import { globalScene } from "#app/global-scene"; +import { allRewards } from "#data/data-lists"; +import type { HeldItemId } from "#enums/held-item-id"; +import { getRewardCategory, RewardCategoryId, RewardId } from "#enums/reward-id"; +import type { RarityTier } from "#enums/reward-tier"; +import type { TrainerItemId } from "#enums/trainer-item-id"; +import type { RewardFunc, RewardPoolId, RewardSpecs } from "#types/rewards"; +import { heldItemRarities } from "./held-item-default-tiers"; +import { + HeldItemReward, + type PokemonMoveReward, + type RememberMoveReward, + type Reward, + RewardGenerator, + RewardOption, + type TmReward, + TrainerItemReward, +} from "./reward"; +import { rewardRarities } from "./reward-defaults-tiers"; +import { trainerItemRarities } from "./trainer-item-default-tiers"; + +export function isTmReward(reward: Reward): reward is TmReward { + return getRewardCategory(reward.id) === RewardCategoryId.TM; +} + +export function isMoveReward(reward: Reward): reward is PokemonMoveReward { + const categoryId = getRewardCategory(reward.id); + return categoryId === RewardCategoryId.ETHER || categoryId === RewardCategoryId.PP_UP; +} + +export function isRememberMoveReward(reward: Reward): reward is RememberMoveReward { + return reward.id === RewardId.MEMORY_MUSHROOM; +} + +/** + * Generates a Reward from a given function + * @param rewardFunc + * @param pregenArgs Can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. + */ +export function generateReward(rewardFunc: RewardFunc, pregenArgs?: any[]): Reward | null { + const reward = rewardFunc(); + return reward instanceof RewardGenerator ? reward.generateReward(globalScene.getPlayerParty(), pregenArgs) : reward; +} + +export function generateRewardOptionFromId( + id: RewardPoolId, + cost = 0, + tierOverride?: RarityTier, + upgradeCount = 0, + pregenArgs?: any[], +): RewardOption | null { + if (isHeldItemId(id)) { + const reward = new HeldItemReward(id); + const tier = tierOverride ?? heldItemRarities[id]; + return new RewardOption(reward, upgradeCount, tier, cost); + } + + if (isTrainerItemId(id)) { + const reward = new TrainerItemReward(id); + const tier = tierOverride ?? trainerItemRarities[id]; + return new RewardOption(reward, upgradeCount, tier, cost); + } + + const rewardFunc = allRewards[id]; + const reward = generateReward(rewardFunc, pregenArgs); + if (reward) { + const tier = tierOverride ?? rewardRarities[id]; + return new RewardOption(reward, upgradeCount, tier, cost); + } + return null; +} + +export function generateRewardOptionFromSpecs( + specs: RewardSpecs, + cost = 0, + overrideTier?: RarityTier, +): RewardOption | null { + if (typeof specs === "number") { + return generateRewardOptionFromId(specs, cost, overrideTier); + } + return generateRewardOptionFromId(specs.id, cost, overrideTier, 0, specs.args); +} + +export function getPlayerShopRewardOptionsForWave(waveIndex: number, baseCost: number): RewardOption[] { + if (!(waveIndex % 10)) { + return []; + } + + const options = [ + [ + generateRewardOptionFromId(RewardId.POTION, baseCost * 0.2), + generateRewardOptionFromId(RewardId.ETHER, baseCost * 0.4), + generateRewardOptionFromId(RewardId.REVIVE, baseCost * 2), + ], + [ + generateRewardOptionFromId(RewardId.SUPER_POTION, baseCost * 0.45), + generateRewardOptionFromId(RewardId.FULL_HEAL, baseCost), + ], + [generateRewardOptionFromId(RewardId.ELIXIR, baseCost), generateRewardOptionFromId(RewardId.MAX_ETHER, baseCost)], + [ + generateRewardOptionFromId(RewardId.HYPER_POTION, baseCost * 0.8), + generateRewardOptionFromId(RewardId.MAX_REVIVE, baseCost * 2.75), + generateRewardOptionFromId(RewardId.MEMORY_MUSHROOM, baseCost * 4), + ], + [ + generateRewardOptionFromId(RewardId.MAX_POTION, baseCost * 1.5), + generateRewardOptionFromId(RewardId.MAX_ELIXIR, baseCost * 2.5), + ], + [generateRewardOptionFromId(RewardId.FULL_RESTORE, baseCost * 2.25)], + [generateRewardOptionFromId(RewardId.SACRED_ASH, baseCost * 10)], + ]; + return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat(); +} + +export function isRewardId(id: RewardPoolId): id is RewardId { + return id > 0x2000; +} + +export function isTrainerItemId(id: RewardPoolId): id is TrainerItemId { + return id > 0x1000 && id < 0x2000; +} + +export function isHeldItemId(id: RewardPoolId): id is HeldItemId { + return id < 0x1000; +} diff --git a/src/items/reward.ts b/src/items/reward.ts new file mode 100644 index 00000000000..b4fcc9a1735 --- /dev/null +++ b/src/items/reward.ts @@ -0,0 +1,1541 @@ +import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { EvolutionItem, FusionSpeciesFormEvolution, pokemonEvolutions } from "#balance/pokemon-evolutions"; +import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#balance/starters"; +import { tmPoolTiers, tmSpecies } from "#balance/tms"; +import { allHeldItems, allMoves, allTrainerItems } from "#data/data-lists"; +import { getLevelTotalExp } from "#data/exp"; +import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; +import { getNatureName, getNatureStatMultiplier } from "#data/nature"; +import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS } from "#data/pokeball"; +import { pokemonFormChanges, SpeciesFormChangeCondition } from "#data/pokemon-forms"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BerryType } from "#enums/berry-type"; +import { FormChangeItem } from "#enums/form-change-item"; +import { HeldItemId } from "#enums/held-item-id"; +import { LearnMoveType } from "#enums/learn-move-type"; +import { MoveId } from "#enums/move-id"; +import { Nature } from "#enums/nature"; +import type { PokeballType } from "#enums/pokeball"; +import { PokemonType } from "#enums/pokemon-type"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; +import { SpeciesFormKey } from "#enums/species-form-key"; +import { SpeciesId } from "#enums/species-id"; +import type { PermanentStat, TempBattleStat } from "#enums/stat"; +import { Stat, TEMP_BATTLE_STATS } from "#enums/stat"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import type { PlayerPokemon, Pokemon } from "#field/pokemon"; +import { attackTypeToHeldItem } from "#items/attack-type-booster"; +import { permanentStatToHeldItem, statBoostItems } from "#items/base-stat-booster"; +import { berryTypeToHeldItem } from "#items/berry"; +import { getNewAttackTypeBoosterHeldItem, getNewBerryHeldItem, getNewVitaminHeldItem } from "#items/held-item-pool"; +import { formChangeItemName } from "#items/item-utility"; +import { SPECIES_STAT_BOOSTER_ITEMS, type SpeciesStatBoostHeldItem } from "#items/stat-booster"; +import { TrainerItemEffect, tempStatToTrainerItem } from "#items/trainer-item"; +import type { PokemonMove } from "#moves/pokemon-move"; +import { getVoucherTypeIcon, getVoucherTypeName, type VoucherType } from "#system/voucher"; +import type { Exact } from "#types/type-helpers"; +import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#ui/party-ui-handler"; +import { PartyUiHandler } from "#ui/party-ui-handler"; +import { formatMoney, NumberHolder, padInt, randSeedInt, randSeedItem } from "#utils/common"; +import { getEnumKeys, getEnumValues } from "#utils/enums"; +import i18next from "i18next"; + +/* +The term "Reward" refers to items the player can access in the post-battle screen (although +they may be used in other places of the code as well). + +Examples include (but are not limited to): +- Potions and other healing items +- Held items and trainer items +- Money items such as nugget and ancient relic + +Rewards have a basic structure with a name, description, and icon. These are used to display +the reward in the reward select screen. All rewards have an .apply() method, which applies the +effect, for example: +- Apply healing to a pokemon +- Assign a held item to a pokemon, or a trainer item to the player +- Add money + +Some rewards, once clicked, simply have their effect---these are Rewards that add money, pokéball, +vouchers, or global effect such as Sacred Ash. +Most rewards require extra parameters. They are divided into subclasses depending on the parameters +that they need, in particular: +- PokemonReward requires to pass a Pokemon (to apply healing, assign item...) +- PokemonMoveReward requires to pass a Pokemon and a move (for Elixir, or PP Up) +Plus some edge cases for Memory Mushroom and DNA Splicers. + +The parameters to be passed are generated by the .applyReward() function in SelectRewardPhase. +This function takes care of opening the party screen and letting the player select a party pokemon, +a move, etc. depending on what is required. Once the parameters are generated, instead of calling +.apply() directly, we call the .applyReward() method in BattleScene, which also plays the sound. +[This method could perhaps be removed]. + +Rewards are assigned RewardId, and there are also RewardCategoryId. For example, TM is a RewardCategoryId, +while CommonTM, RareTM etc are RewardIds. There is _not_ a RewardId for _each_ move. Similarly, +some specific categories of held items are assigned their own RewardId, but they all fall under a single +RewardCategoryId. + +rewardInitObj plays a similar role to allHeldItems, except instead of containing all possible reward +instances, it instead contains functions that generate those rewards. Here, the keys used are strings +rather than RewardId, the difference exists because here we want to distinguish unique held items +for example. The entries of rewardInitObj are used in the RewardPool. + +There are some more derived classes, in particular: +RewardGenerator, which creates Reward instances from a certain group (e.g. TMs, nature mints, or berries); +and RewardOption, which is displayed during the select reward phase at the end of each encounter. +*/ + +export abstract class Reward { + // TODO: If all we care about for categorization is the reward's ID's _category_, why not do it there? + // TODO: Make abstract and readonly + public id: RewardId; + public localeKey: string; + public iconImage: string; + public group: string; // TODO: Make a union type of all groups + public soundName: string; + public tier: RarityTier; + + constructor(localeKey: string | null, iconImage: string | null, group?: string, soundName?: string) { + this.localeKey = localeKey!; // TODO: is this bang correct? + this.iconImage = iconImage!; // TODO: is this bang correct? + this.group = group!; // TODO: is this bang correct? + this.soundName = soundName ?? "se/restore"; + } + + get name(): string { + return i18next.t(`${this.localeKey}.name`); + } + + getDescription(): string { + return i18next.t(`${this.localeKey}.description`); + } + + getIcon(): string { + return this.iconImage; + } + + // TODO: Should this be abstract? + /** + * Check whether this reward should be applied. + */ + // TODO: This is erroring on stuff of typ + shouldApply(_params: Exact[0]>): boolean { + return true; + } + + /** Apply this Reward's effects. */ + // TODO: Remove `boolean` return from all superclasses' type signatures + abstract apply(_params?: unknown): void; +} + +// TODO: Can this return null? +// TODO: Make this generic based on T +type RewardGeneratorFunc = (party: Pokemon[], pregenArgs?: any[]) => T | null; + +export abstract class RewardGenerator { + private genRewardFunc: RewardGeneratorFunc; + public id: RewardId; + + constructor(genRewardFunc: RewardGeneratorFunc) { + this.genRewardFunc = genRewardFunc; + } + + generateReward(party: Pokemon[], pregenArgs?: any[]) { + const ret = this.genRewardFunc(party, pregenArgs); + if (ret && this.id) { + ret.id = this.id; + } + return ret; + } +} + +export class AddPokeballReward extends Reward { + private pokeballType: PokeballType; + private count: number; + + constructor(iconImage: string, pokeballType: PokeballType, count: number, id: RewardId) { + super("", iconImage, "pb", "se/pb_bounce_1"); + this.pokeballType = pokeballType; + this.count = count; + this.id = id; + } + + get name(): string { + return i18next.t("modifierType:ModifierType.AddPokeballModifierType.name", { + modifierCount: this.count, + pokeballName: getPokeballName(this.pokeballType), + }); + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.AddPokeballModifierType.description", { + modifierCount: this.count, + pokeballName: getPokeballName(this.pokeballType), + catchRate: + getPokeballCatchMultiplier(this.pokeballType) > -1 + ? `${getPokeballCatchMultiplier(this.pokeballType)}x` + : "100%", + pokeballAmount: `${globalScene.pokeballCounts[this.pokeballType]}`, + }); + } + + /** + * Applies {@linkcode AddPokeballReward} + * @returns always `true` + */ + apply(): boolean { + const pokeballCounts = globalScene.pokeballCounts; + pokeballCounts[this.pokeballType] = Math.min( + pokeballCounts[this.pokeballType] + this.count, + MAX_PER_TYPE_POKEBALLS, + ); + + return true; + } +} + +export class AddVoucherReward extends Reward { + private voucherType: VoucherType; + private count: number; + + constructor(voucherType: VoucherType, count: number, id: RewardId) { + super("", getVoucherTypeIcon(voucherType), "voucher"); + this.count = count; + this.voucherType = voucherType; + this.id = id; + } + + get name(): string { + return i18next.t("modifierType:ModifierType.AddVoucherConsumableType.name", { + modifierCount: this.count, + voucherTypeName: getVoucherTypeName(this.voucherType), + }); + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.AddVoucherConsumableType.description", { + modifierCount: this.count, + voucherTypeName: getVoucherTypeName(this.voucherType), + }); + } + + /** + * Applies {@linkcode AddVoucherReward} + * @param battleScene {@linkcode BattleScene} + * @returns always `true` + */ + apply(): boolean { + const voucherCounts = globalScene.gameData.voucherCounts; + voucherCounts[this.voucherType] += this.count; + + return true; + } +} + +export class AddMoneyReward extends Reward { + private moneyMultiplier: number; + private moneyMultiplierDescriptorKey: string; + + constructor( + localeKey: string, + iconImage: string, + moneyMultiplier: number, + moneyMultiplierDescriptorKey: string, + id: RewardId, + ) { + super(localeKey, iconImage, "money", "se/buy"); + + this.moneyMultiplier = moneyMultiplier; + this.moneyMultiplierDescriptorKey = moneyMultiplierDescriptorKey; + this.id = id; + } + + getDescription(): string { + const moneyAmount = new NumberHolder(globalScene.getWaveMoneyAmount(this.moneyMultiplier)); + globalScene.applyPlayerItems(TrainerItemEffect.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); + const formattedMoney = formatMoney(globalScene.moneyFormat, moneyAmount.value); + + return i18next.t("modifierType:ModifierType.MoneyRewardModifierType.description", { + moneyMultiplier: i18next.t(this.moneyMultiplierDescriptorKey as any), + moneyAmount: formattedMoney, + }); + } + + /** + * Applies {@linkcode AddMoneyReward} + * @returns always `true` + */ + apply(): boolean { + const moneyAmount = new NumberHolder(globalScene.getWaveMoneyAmount(this.moneyMultiplier)); + + globalScene.applyPlayerItems(TrainerItemEffect.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); + + globalScene.addMoney(moneyAmount.value); + + for (const p of globalScene.getPlayerParty()) { + if (p.hasSpecies(SpeciesId.GIMMIGHOUL)) { + const factor = Math.min(Math.floor(this.moneyMultiplier), 3); + p.heldItemManager.add(HeldItemId.GIMMIGHOUL_EVO_TRACKER, factor); + } + } + + return true; + } +} + +/** Rewards that are applied to individual Pokemon. */ +export abstract class PokemonReward extends Reward { + public selectFilter: PokemonSelectFilter | undefined; + + constructor( + localeKey: string, + iconImage: string, + selectFilter?: PokemonSelectFilter, + group?: string, + soundName?: string, + ) { + super(localeKey, iconImage, group, soundName); + this.selectFilter = selectFilter; + } + + abstract override apply(_params: PokemonRewardParams): void; +} + +export interface PokemonRewardParams { + pokemon: PlayerPokemon; +} + +export interface PokemonMoveRewardParams { + pokemon: PlayerPokemon; + moveIndex: number; +} + +export interface PokemonMoveRecallRewardParams { + pokemon: PlayerPokemon; + moveIndex: number; + cost?: number; +} + +export interface PokemonFusionRewardParams { + pokemon: PlayerPokemon; + pokemon2: PlayerPokemon; +} + +export class HeldItemReward extends PokemonReward { + public itemId: HeldItemId; + constructor(itemId: HeldItemId, group?: string, soundName?: string) { + super( + "", + "", + (pokemon: PlayerPokemon) => { + 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 (hasItem && pokemon.heldItemManager.getStack(this.itemId) === maxStackCount) { + return i18next.t("modifierType:ModifierType.PokemonHeldItemModifierType.extra.tooMany", { + pokemonName: getPokemonNameWithAffix(pokemon), + }); + } + return null; + }, + group, + soundName, + ); + this.itemId = itemId; + this.id = RewardId.HELD_ITEM; + } + + get name(): string { + return allHeldItems[this.itemId].name; + } + + getDescription(): string { + return allHeldItems[this.itemId].description; + } + + getIcon(): string { + return allHeldItems[this.itemId].iconName; + } + + apply({ pokemon }: PokemonRewardParams): boolean { + return pokemon.heldItemManager.add(this.itemId); + } +} + +export class TrainerItemReward extends Reward { + public itemId: TrainerItemId; + constructor(itemId: TrainerItemId, group?: string, soundName?: string) { + super("", "", group, soundName); + this.itemId = itemId; + this.id = RewardId.TRAINER_ITEM; + } + + get name(): string { + return allTrainerItems[this.itemId].name; + } + + getDescription(): string { + return allTrainerItems[this.itemId].description; + } + + getIcon(): string { + return allTrainerItems[this.itemId].iconName; + } + + apply(): boolean { + return globalScene.trainerItems.add(this.itemId); + } +} + +export class LapsingTrainerItemReward extends TrainerItemReward { + constructor(itemId: TrainerItemId, id?: RewardId) { + super(itemId); + this.id = id ?? RewardId.TRAINER_ITEM; + } + + apply(): boolean { + return globalScene.trainerItems.add(this.itemId, allTrainerItems[this.itemId].getMaxStackCount()); + } +} + +export class ChangeTeraTypeReward extends PokemonReward { + private teraType: PokemonType; + + constructor(teraType: PokemonType) { + super( + "", + `${PokemonType[teraType].toLowerCase()}_tera_shard`, + (pokemon: PlayerPokemon) => { + if ( + [pokemon.species.speciesId, pokemon.fusionSpecies?.speciesId].filter( + s => s === SpeciesId.TERAPAGOS || s === SpeciesId.OGERPON || s === SpeciesId.SHEDINJA, + ).length > 0 + ) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }, + "tera_shard", + ); + + this.teraType = teraType; + } + + get name(): string { + return i18next.t("modifierType:ModifierType.ChangeTeraTypeModifierType.name", { + teraType: i18next.t(`pokemonInfo:Type.${PokemonType[this.teraType]}`), + }); + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.ChangeTeraTypeModifierType.description", { + teraType: i18next.t(`pokemonInfo:Type.${PokemonType[this.teraType]}`), + }); + } + + getPregenArgs(): any[] { + return [this.teraType]; + } + + /** + * Checks if {@linkcode TerrastalizeConsumable} should be applied + * @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item + * @returns `true` if the {@linkcode TerrastalizeConsumable} should be applied + */ + shouldApply({ pokemon }: PokemonRewardParams): boolean { + return ( + pokemon.teraType !== this.teraType && + ![SpeciesId.SHEDINJA, SpeciesId.OGERPON, SpeciesId.TERAPAGOS].some(s => pokemon.hasSpecies(s)) + ); + } + + /** + * Applies {@linkcode TerrastalizeConsumable} + * @param pokemon The {@linkcode PlayerPokemon} that consumes the item + * @returns `true` if hp was restored + */ + apply({ pokemon }: PokemonRewardParams): boolean { + pokemon.teraType = this.teraType; + return true; + } +} + +function restorePokemonHp( + pokemon: Pokemon, + percentToRestore: number, + pointsToRestore = 0, + healStatus = false, + fainted = false, +): boolean { + if (!pokemon.hp === fainted) { + if (fainted || healStatus) { + pokemon.resetStatus(true, true, false, false); + } + // Apply HealingCharm + let multiplier = 1; + if (!fainted) { + const hpRestoreMultiplier = new NumberHolder(1); + this.applyPlayerItems(TrainerItemEffect.HEALING_BOOSTER, { numberHolder: hpRestoreMultiplier }); + multiplier = hpRestoreMultiplier.value; + } + const restorePoints = Math.floor(pointsToRestore * multiplier); + const restorePercent = Math.floor(percentToRestore * 0.01 * multiplier * pokemon.getMaxHp()); + pokemon.heal(Math.max(restorePercent, restorePoints, 1)); + return true; + } + return false; +} + +export class PokemonHpRestoreReward extends PokemonReward { + protected restorePoints: number; + protected restorePercent: number; + protected healStatus: boolean; + + constructor( + localeKey: string, + iconImage: string, + id: RewardId, + restorePoints: number, + restorePercent: number, + healStatus = false, + selectFilter?: PokemonSelectFilter, + group?: string, + ) { + super( + localeKey, + iconImage, + selectFilter || + ((pokemon: PlayerPokemon) => { + if ( + !pokemon.hp || + (pokemon.isFullHp() && (!this.healStatus || (!pokemon.status && !pokemon.getTag(BattlerTagType.CONFUSED)))) + ) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }), + group || "potion", + ); + + this.restorePoints = restorePoints; + this.restorePercent = restorePercent; + this.healStatus = healStatus; + this.id = id; + } + + getDescription(): string { + return this.restorePoints + ? i18next.t("modifierType:ModifierType.PokemonHpRestoreModifierType.description", { + restorePoints: this.restorePoints, + restorePercent: this.restorePercent, + }) + : this.healStatus + ? i18next.t("modifierType:ModifierType.PokemonHpRestoreModifierType.extra.fullyWithStatus") + : i18next.t("modifierType:ModifierType.PokemonHpRestoreModifierType.extra.fully"); + } + + apply({ pokemon }: PokemonRewardParams): boolean { + return restorePokemonHp(pokemon, this.restorePercent, this.restorePoints, this.healStatus, false); + } +} + +export class PokemonReviveReward extends PokemonHpRestoreReward { + constructor(localeKey: string, iconImage: string, id: RewardId, restorePercent: number) { + super( + localeKey, + iconImage, + id, + 0, + restorePercent, + false, + (pokemon: PlayerPokemon) => { + if (!pokemon.isFainted()) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }, + "revive", + ); + + this.selectFilter = (pokemon: PlayerPokemon) => { + if (pokemon.hp) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }; + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.PokemonReviveModifierType.description", { + restorePercent: this.restorePercent, + }); + } + + apply({ pokemon }: PokemonRewardParams): boolean { + return restorePokemonHp(pokemon, this.restorePercent, 0, false, true); + } +} + +export class AllPokemonFullReviveReward extends Reward { + constructor(localeKey: string, iconImage: string) { + super(localeKey, iconImage, "modifierType:ModifierType.AllPokemonFullReviveModifierType"); + this.id = RewardId.SACRED_ASH; + } + + apply(): boolean { + for (const pokemon of globalScene.getPlayerParty()) { + restorePokemonHp(pokemon, 100, 0, false, true); + } + + return true; + } +} + +export class PokemonStatusHealReward extends PokemonReward { + constructor(localeKey: string, iconImage: string) { + super(localeKey, iconImage, (pokemon: PlayerPokemon) => { + if (!pokemon.hp || (!pokemon.status && !pokemon.getTag(BattlerTagType.CONFUSED))) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }); + this.id = RewardId.FULL_HEAL; + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.PokemonStatusHealModifierType.description"); + } + + apply({ pokemon }: PokemonRewardParams): boolean { + pokemon.resetStatus(true, true, false, false); + return true; + } +} + +export abstract class PokemonMoveReward extends PokemonReward { + public moveSelectFilter: PokemonMoveSelectFilter | undefined; + + constructor( + localeKey: string, + iconImage: string, + id: RewardId, + selectFilter?: PokemonSelectFilter, + moveSelectFilter?: PokemonMoveSelectFilter, + group?: string, + ) { + super(localeKey, iconImage, selectFilter, group); + this.moveSelectFilter = moveSelectFilter; + this.id = id; + } + + apply(_params: PokemonMoveRewardParams): boolean { + return false; + } +} + +export class PokemonPpRestoreReward extends PokemonMoveReward { + protected restorePoints: number; + + constructor(localeKey: string, iconImage: string, id: RewardId, restorePoints: number) { + super( + localeKey, + iconImage, + id, + (_pokemon: PlayerPokemon) => { + return null; + }, + (pokemonMove: PokemonMove) => { + if (!pokemonMove.ppUsed) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }, + "ether", + ); + + this.restorePoints = restorePoints; + } + + getDescription(): string { + return this.restorePoints > -1 + ? i18next.t("modifierType:ModifierType.PokemonPpRestoreModifierType.description", { + restorePoints: this.restorePoints, + }) + : i18next.t("modifierType:ModifierType.PokemonPpRestoreModifierType.extra.fully"); + } + + /** + * Applies {@linkcode PokemonPpRestoreConsumable} + * @param playerPokemon The {@linkcode PlayerPokemon} that should get move pp restored + * @returns always `true` + */ + apply({ pokemon, moveIndex }: PokemonMoveRewardParams): boolean { + const move = pokemon.getMoveset()[moveIndex]; + + if (move) { + move.ppUsed = this.restorePoints > -1 ? Math.max(move.ppUsed - this.restorePoints, 0) : 0; + } + + return true; + } +} + +export class PokemonAllMovePpRestoreReward extends PokemonReward { + protected restorePoints: number; + + constructor(localeKey: string, iconImage: string, id: RewardId, restorePoints: number) { + super( + localeKey, + iconImage, + (pokemon: PlayerPokemon) => { + if (!pokemon.getMoveset().filter(m => m.ppUsed).length) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }, + "elixir", + ); + + this.restorePoints = restorePoints; + this.id = id; + } + + getDescription(): string { + return this.restorePoints > -1 + ? i18next.t("modifierType:ModifierType.PokemonAllMovePpRestoreModifierType.description", { + restorePoints: this.restorePoints, + }) + : i18next.t("modifierType:ModifierType.PokemonAllMovePpRestoreModifierType.extra.fully"); + } + + /** + * Applies {@linkcode PokemonAllMovePpRestoreConsumable} + * @param playerPokemon The {@linkcode PlayerPokemon} that should get all move pp restored + * @returns always `true` + */ + apply({ pokemon }: PokemonRewardParams): boolean { + for (const move of pokemon.getMoveset()) { + if (move) { + move.ppUsed = this.restorePoints > -1 ? Math.max(move.ppUsed - this.restorePoints, 0) : 0; + } + } + + return true; + } +} + +export class PokemonPpUpReward extends PokemonMoveReward { + protected upPoints: number; + + constructor(localeKey: string, iconImage: string, id: RewardId, upPoints: number) { + super( + localeKey, + iconImage, + id, + (_pokemon: PlayerPokemon) => { + return null; + }, + (pokemonMove: PokemonMove) => { + if (pokemonMove.getMove().pp < 5 || pokemonMove.ppUp >= 3 || pokemonMove.maxPpOverride) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }, + "ppUp", + ); + + this.upPoints = upPoints; + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.PokemonPpUpModifierType.description", { upPoints: this.upPoints }); + } + + /** + * Applies {@linkcode PokemonPpUpConsumable} + * @param playerPokemon The {@linkcode PlayerPokemon} that gets a pp up on move-slot {@linkcode moveIndex} + * @returns + */ + apply({ pokemon, moveIndex }: PokemonMoveRewardParams): boolean { + const move = pokemon.getMoveset()[moveIndex]; + + if (move && !move.maxPpOverride) { + move.ppUp = Math.min(move.ppUp + this.upPoints, 3); + } + + return true; + } +} + +export class PokemonNatureChangeReward extends PokemonReward { + protected nature: Nature; + + constructor(nature: Nature) { + super( + "", + `mint_${ + getEnumKeys(Stat) + .find(s => getNatureStatMultiplier(nature, Stat[s]) > 1) + ?.toLowerCase() || "neutral" + }`, + (pokemon: PlayerPokemon) => { + if (pokemon.getNature() === this.nature) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }, + "mint", + ); + + this.nature = nature; + this.id = RewardId.MINT; + } + + get name(): string { + return i18next.t("modifierType:ModifierType.PokemonNatureChangeModifierType.name", { + natureName: getNatureName(this.nature), + }); + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.PokemonNatureChangeModifierType.description", { + natureName: getNatureName(this.nature, true, true, true), + }); + } + + /** + * Applies {@linkcode PokemonNatureChangeConsumable} + * @param playerPokemon {@linkcode PlayerPokemon} to apply the {@linkcode Nature} change to + * @returns + */ + apply({ pokemon }: PokemonRewardParams): boolean { + pokemon.setCustomNature(this.nature); + globalScene.gameData.unlockSpeciesNature(pokemon.species, this.nature); + + return true; + } +} + +export class RememberMoveReward extends PokemonReward { + constructor(localeKey: string, iconImage: string, group?: string) { + super( + localeKey, + iconImage, + (pokemon: PlayerPokemon) => { + if (!pokemon.getLearnableLevelMoves().length) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }, + group, + ); + this.id = RewardId.MEMORY_MUSHROOM; + } + + /** + * Applies {@linkcode RememberMoveConsumable} + * @param playerPokemon The {@linkcode PlayerPokemon} that should remember the move + * @returns always `true` + */ + apply({ pokemon, moveIndex, cost }: PokemonMoveRecallRewardParams): boolean { + globalScene.phaseManager.unshiftNew( + "LearnMovePhase", + globalScene.getPlayerParty().indexOf(pokemon as PlayerPokemon), + pokemon.getLearnableLevelMoves()[moveIndex], + LearnMoveType.MEMORY, + cost, + ); + + return true; + } +} + +export class BerryRewardGenerator extends RewardGenerator { + 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); + }); + this.id = RewardId.BERRY; + } +} + +export class MintRewardGenerator extends RewardGenerator { + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in Nature) { + return new PokemonNatureChangeReward(pregenArgs[0] as Nature); + } + return new PokemonNatureChangeReward(randSeedItem(getEnumValues(Nature))); + }); + this.id = RewardId.MINT; + } +} + +export class TeraTypeRewardGenerator extends RewardGenerator { + constructor() { + super((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) { + return new ChangeTeraTypeReward(pregenArgs[0] as PokemonType); + } + if (!globalScene.trainerItems.hasItem(TrainerItemId.TERA_ORB)) { + return null; + } + const teraTypes: PokemonType[] = []; + for (const p of party) { + if ( + !(p.hasSpecies(SpeciesId.TERAPAGOS) || p.hasSpecies(SpeciesId.OGERPON) || p.hasSpecies(SpeciesId.SHEDINJA)) + ) { + teraTypes.push(p.teraType); + } + } + let excludedType = PokemonType.UNKNOWN; + if (teraTypes.length > 0 && teraTypes.filter(t => t === teraTypes[0]).length === teraTypes.length) { + excludedType = teraTypes[0]; + } + let shardType = randSeedInt(64) ? (randSeedInt(18) as PokemonType) : PokemonType.STELLAR; + while (shardType === excludedType) { + shardType = randSeedInt(64) ? (randSeedInt(18) as PokemonType) : PokemonType.STELLAR; + } + return new ChangeTeraTypeReward(shardType); + }); + this.id = RewardId.TERA_SHARD; + } +} + +export class AttackTypeBoosterReward extends HeldItemReward { + public moveType: PokemonType; + public boostPercent: number; + + constructor(moveType: PokemonType, boostPercent: number) { + const itemId = attackTypeToHeldItem[moveType]; + super(itemId); + this.moveType = moveType; + this.boostPercent = boostPercent; + } + + getPregenArgs(): any[] { + return [this.moveType]; + } +} + +function incrementLevelWithCandy(pokemon: Pokemon): boolean { + const levelCount = new NumberHolder(1); + globalScene.applyPlayerItems(TrainerItemEffect.LEVEL_INCREMENT_BOOSTER, { numberHolder: levelCount }); + + pokemon.level += levelCount.value; + if (pokemon.level <= globalScene.getMaxExpLevel(true)) { + pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); + pokemon.levelExp = 0; + } + + if (pokemon.isPlayer()) { + pokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY); + + globalScene.phaseManager.unshiftNew( + "LevelUpPhase", + globalScene.getPlayerParty().indexOf(pokemon), + pokemon.level - levelCount.value, + pokemon.level, + ); + } + return true; +} + +export class PokemonLevelIncrementReward extends PokemonReward { + constructor(localeKey: string, iconImage: string) { + super(localeKey, iconImage, (_pokemon: PlayerPokemon) => null); + this.id = RewardId.RARE_CANDY; + } + + getDescription(): string { + let levels = 1; + const candyJarStack = globalScene.trainerItems.getStack(TrainerItemId.CANDY_JAR); + levels += candyJarStack; + return i18next.t("modifierType:ModifierType.PokemonLevelIncrementModifierType.description", { levels }); + } + + /** + * Applies {@linkcode PokemonLevelIncrementConsumable} + * @param playerPokemon The {@linkcode PlayerPokemon} that should get levels incremented + * @param levelCount The amount of levels to increment + * @returns always `true` + */ + apply({ pokemon }: PokemonRewardParams): boolean { + return incrementLevelWithCandy(pokemon); + } +} + +export class AllPokemonLevelIncrementReward extends Reward { + id = RewardId.RARER_CANDY; + + getDescription(): string { + let levels = 1; + const candyJarStack = globalScene.trainerItems.getStack(TrainerItemId.CANDY_JAR); + levels += candyJarStack; + return i18next.t("modifierType:ModifierType.AllPokemonLevelIncrementModifierType.description", { levels }); + } + + apply(): boolean { + for (const pokemon of globalScene.getPlayerParty()) { + incrementLevelWithCandy(pokemon); + } + + return true; + } +} + +export class BaseStatBoosterReward extends HeldItemReward { + private stat: PermanentStat; + private key: string; + + constructor(stat: PermanentStat) { + const key = statBoostItems[stat]; + const itemId = permanentStatToHeldItem[stat]; + super(itemId); + + this.stat = stat; + this.key = key; + } +} + +export class TmReward extends PokemonReward { + public moveId: MoveId; + + constructor(moveId: MoveId) { + super( + "", + `tm_${PokemonType[allMoves[moveId].type].toLowerCase()}`, + (pokemon: PlayerPokemon) => { + if ( + pokemon.compatibleTms.indexOf(moveId) === -1 || + pokemon.getMoveset().filter(m => m.moveId === moveId).length + ) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }, + "tm", + ); + + this.moveId = moveId; + } + + get name(): string { + return i18next.t("modifierType:ModifierType.TmModifierType.name", { + moveId: padInt(Object.keys(tmSpecies).indexOf(this.moveId.toString()) + 1, 3), + moveName: allMoves[this.moveId].name, + }); + } + + getDescription(): string { + return i18next.t( + globalScene.enableMoveInfo + ? "modifierType:ModifierType.TmModifierTypeWithInfo.description" + : "modifierType:ModifierType.TmModifierType.description", + { moveName: allMoves[this.moveId].name }, + ); + } + + /** + * Applies {@linkcode TmConsumable} + * @param playerPokemon The {@linkcode PlayerPokemon} that should learn the TM + * @returns always `true` + */ + apply({ pokemon }: PokemonRewardParams): boolean { + globalScene.phaseManager.unshiftNew( + "LearnMovePhase", + globalScene.getPlayerParty().indexOf(pokemon), + this.moveId, + LearnMoveType.TM, + ); + + return true; + } +} + +export class EvolutionItemReward extends PokemonReward { + public evolutionItem: EvolutionItem; + + constructor(evolutionItem: EvolutionItem) { + super("", EvolutionItem[evolutionItem].toLowerCase(), (pokemon: PlayerPokemon) => { + if ( + pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && + pokemonEvolutions[pokemon.species.speciesId].filter(e => e.validate(pokemon, false, this.evolutionItem)) + .length && + pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX + ) { + return null; + } + if ( + pokemon.isFusion() && + pokemon.fusionSpecies && + pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) && + pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(e => e.validate(pokemon, true, this.evolutionItem)) + .length && + pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX + ) { + return null; + } + + return PartyUiHandler.NoEffectMessage; + }); + + this.evolutionItem = evolutionItem; + } + + get name(): string { + return i18next.t(`modifierType:EvolutionItem.${EvolutionItem[this.evolutionItem]}`); + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.EvolutionItemModifierType.description"); + } + + getPregenArgs(): any[] { + return [this.evolutionItem]; + } + + /** + * Applies {@linkcode EvolutionItemConsumable} + * @param playerPokemon The {@linkcode PlayerPokemon} that should evolve via item + * @returns `true` if the evolution was successful + */ + apply({ pokemon }: PokemonRewardParams): boolean { + let matchingEvolution = pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) + ? pokemonEvolutions[pokemon.species.speciesId].find( + e => e.evoItem === this.evolutionItem && e.validate(pokemon, false, e.item!), + ) + : null; + + if (!matchingEvolution && pokemon.isFusion()) { + matchingEvolution = pokemonEvolutions[pokemon.fusionSpecies!.speciesId].find( + e => e.evoItem === this.evolutionItem && e.validate(pokemon, true, e.item!), + ); + if (matchingEvolution) { + matchingEvolution = new FusionSpeciesFormEvolution(pokemon.species.speciesId, matchingEvolution); + } + } + + if (matchingEvolution) { + globalScene.phaseManager.unshiftNew("EvolutionPhase", pokemon, matchingEvolution, pokemon.level - 1); + return true; + } + + return false; + } +} + +/** + * Class that represents form changing items + */ +export class FormChangeItemReward extends PokemonReward { + public formChangeItem: FormChangeItem; + + constructor(formChangeItem: FormChangeItem) { + super("", FormChangeItem[formChangeItem].toLowerCase(), (pokemon: PlayerPokemon) => { + // Make sure the Pokemon has alternate forms + if ( + pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId) && + // Get all form changes for this species with an item trigger, including any compound triggers + pokemonFormChanges[pokemon.species.speciesId] + .filter( + fc => fc.trigger.hasTriggerType(SpeciesFormChangeItemTrigger) && fc.preFormKey === pokemon.getFormKey(), + ) + // Returns true if any form changes match this item + .flatMap(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) + .flatMap(fc => fc.item) + .includes(this.formChangeItem) + ) { + return null; + } + + return PartyUiHandler.NoEffectMessage; + }); + + this.formChangeItem = formChangeItem; + this.id = RewardId.FORM_CHANGE_ITEM; + } + + get name(): string { + return formChangeItemName(this.formChangeItem); + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.FormChangeItemModifierType.description"); + } + + apply({ pokemon }: PokemonRewardParams): boolean { + if (pokemon.heldItemManager.hasFormChangeItem(this.formChangeItem)) { + return false; + } + + pokemon.heldItemManager.addFormChangeItem(this.formChangeItem); + pokemon.heldItemManager.toggleActive(this.formChangeItem); + + globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger); + + globalScene.updateItems(true); + + return true; + } +} + +export class FusePokemonReward extends PokemonReward { + constructor(localeKey: string, iconImage: string) { + super(localeKey, iconImage, (pokemon: PlayerPokemon) => { + if (pokemon.isFusion()) { + return PartyUiHandler.NoEffectMessage; + } + return null; + }); + this.id = RewardId.DNA_SPLICERS; + } + + getDescription(): string { + return i18next.t("modifierType:ModifierType.FusePokemonModifierType.description"); + } + + /** + * Applies {@linkcode FusePokemonConsumable} + * @param playerPokemon {@linkcode PlayerPokemon} that should be fused + * @param playerPokemon2 {@linkcode PlayerPokemon} that should be fused with {@linkcode playerPokemon} + * @returns always Promise + */ + apply({ pokemon, pokemon2 }: PokemonFusionRewardParams): boolean { + pokemon.fuse(pokemon2); + return true; + } +} + +export class AttackTypeBoosterRewardGenerator extends RewardGenerator { + constructor() { + super((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) { + return new AttackTypeBoosterReward(pregenArgs[0] as PokemonType, TYPE_BOOST_ITEM_BOOST_PERCENT); + } + + const item = getNewAttackTypeBoosterHeldItem(party); + + return item ? new HeldItemReward(item) : null; + }); + this.id = RewardId.ATTACK_TYPE_BOOSTER; + } +} + +export class BaseStatBoosterRewardGenerator extends RewardGenerator { + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new BaseStatBoosterReward(pregenArgs[0]); + } + return new HeldItemReward(getNewVitaminHeldItem()); + }); + this.id = RewardId.BASE_STAT_BOOSTER; + } +} + +export class TempStatStageBoosterRewardGenerator extends RewardGenerator { + public static readonly items: Record = { + [Stat.ATK]: "x_attack", + [Stat.DEF]: "x_defense", + [Stat.SPATK]: "x_sp_atk", + [Stat.SPDEF]: "x_sp_def", + [Stat.SPD]: "x_speed", + [Stat.ACC]: "x_accuracy", + }; + + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && TEMP_BATTLE_STATS.includes(pregenArgs[0])) { + return new LapsingTrainerItemReward(tempStatToTrainerItem[pregenArgs[0]]); + } + const randStat: TempBattleStat = randSeedInt(Stat.ACC, Stat.ATK); + return new LapsingTrainerItemReward(tempStatToTrainerItem[randStat]); + }); + this.id = RewardId.TEMP_STAT_STAGE_BOOSTER; + } +} + +/** + * Consumable type generator for {@linkcode SpeciesStatBoosterReward}, which + * encapsulates the logic for weighting the most useful held item from + * the current list of {@linkcode items}. + * @extends RewardGenerator + */ +export class SpeciesStatBoosterRewardGenerator extends RewardGenerator { + /** Object comprised of the currently available species-based stat boosting held items */ + + constructor(rare: boolean) { + super((party: Pokemon[], pregenArgs?: any[]) => { + 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 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; + const fusionSpeciesId = p.isFusion() ? p.getFusionSpeciesForm(true).speciesId : null; + // TODO: Use commented boolean when Fling is implemented + const hasFling = false; /* p.getMoveset(true).some(m => m.moveId === MoveId.FLING) */ + + for (const i in tierItems) { + const checkedSpecies = (allHeldItems[tierItems[i]] as SpeciesStatBoostHeldItem).species; + + // If party member already has the item being weighted currently, skip to the next item + const hasItem = p.heldItemManager.hasItem(tierItems[i]); + + if (!hasItem) { + if (checkedSpecies.includes(speciesId) || (!!fusionSpeciesId && checkedSpecies.includes(fusionSpeciesId))) { + // Add weight if party member has a matching species or, if applicable, a matching fusion species + weights[i]++; + } else if (checkedSpecies.includes(SpeciesId.PIKACHU) && hasFling) { + // Add weight to Light Ball if party member has Fling + weights[i]++; + } + } + } + } + + // TODO: Replace this with a helper function + let totalWeight = 0; + for (const weight of weights) { + totalWeight += weight; + } + + if (totalWeight !== 0) { + const randInt = randSeedInt(totalWeight, 1); + let weight = 0; + + for (const i in weights) { + if (weights[i] !== 0) { + const curWeight = weight + weights[i]; + if (randInt <= weight + weights[i]) { + return new HeldItemReward(tierItems[i]); + } + weight = curWeight; + } + } + } + + return null; + }); + this.id = rare ? RewardId.SPECIES_STAT_BOOSTER : RewardId.RARE_SPECIES_STAT_BOOSTER; + } +} + +export class TmRewardGenerator extends RewardGenerator { + constructor(tier: RarityTier) { + super((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in MoveId) { + return new TmReward(pregenArgs[0] as MoveId); + } + const partyMemberCompatibleTms = party.map(p => { + const previousLevelMoves = p.getLearnableLevelMoves(); + return (p as PlayerPokemon).compatibleTms.filter( + tm => !p.moveset.find(m => m.moveId === tm) && !previousLevelMoves.find(lm => lm === tm), + ); + }); + const tierUniqueCompatibleTms = partyMemberCompatibleTms + .flat() + .filter(tm => tmPoolTiers[tm] === tier) + .filter(tm => !allMoves[tm].name.endsWith(" (N)")) + .filter((tm, i, array) => array.indexOf(tm) === i); + if (!tierUniqueCompatibleTms.length) { + return null; + } + // TODO: should this use `randSeedItem`? + const randTmIndex = randSeedInt(tierUniqueCompatibleTms.length); + return new TmReward(tierUniqueCompatibleTms[randTmIndex]); + }); + this.id = + tier === RarityTier.COMMON + ? RewardId.TM_COMMON + : tier === RarityTier.GREAT + ? RewardId.TM_GREAT + : RewardId.TM_ULTRA; + } +} + +export class EvolutionItemRewardGenerator extends RewardGenerator { + constructor(rare: boolean, id: RewardId) { + super((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in EvolutionItem) { + return new EvolutionItemReward(pregenArgs[0] as EvolutionItem); + } + + const evolutionItemPool = [ + party + .filter( + p => + pokemonEvolutions.hasOwnProperty(p.species.speciesId) && + (!p.pauseEvolutions || + p.species.speciesId === SpeciesId.SLOWPOKE || + p.species.speciesId === SpeciesId.EEVEE || + p.species.speciesId === SpeciesId.KIRLIA || + p.species.speciesId === SpeciesId.SNORUNT), + ) + .flatMap(p => { + const evolutions = pokemonEvolutions[p.species.speciesId]; + return evolutions.filter(e => e.isValidItemEvolution(p)); + }), + party + .filter( + p => + p.isFusion() && + p.fusionSpecies && + pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId) && + (!p.pauseEvolutions || + p.fusionSpecies.speciesId === SpeciesId.SLOWPOKE || + p.fusionSpecies.speciesId === SpeciesId.EEVEE || + p.fusionSpecies.speciesId === SpeciesId.KIRLIA || + p.fusionSpecies.speciesId === SpeciesId.SNORUNT), + ) + .flatMap(p => { + const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId]; + return evolutions.filter(e => e.isValidItemEvolution(p, true)); + }), + ] + .flat() + .flatMap(e => e.evoItem) + .filter(i => !!i && i > 50 === rare); + + if (!evolutionItemPool.length) { + return null; + } + + // TODO: should this use `randSeedItem`? + return new EvolutionItemReward(evolutionItemPool[randSeedInt(evolutionItemPool.length)]!); // TODO: is the bang correct? + }); + this.id = id; + } +} + +export class FormChangeItemRewardGenerator extends RewardGenerator { + constructor(isRareFormChangeItem: boolean, id: RewardId) { + super((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in FormChangeItem) { + return new FormChangeItemReward(pregenArgs[0] as FormChangeItem); + } + + const formChangeItemPool = [ + ...new Set( + party + .filter(p => pokemonFormChanges.hasOwnProperty(p.species.speciesId)) + .flatMap(p => { + const formChanges = pokemonFormChanges[p.species.speciesId]; + let formChangeItemTriggers = formChanges + .filter( + fc => + ((fc.formKey.indexOf(SpeciesFormKey.MEGA) === -1 && + fc.formKey.indexOf(SpeciesFormKey.PRIMAL) === -1) || + globalScene.trainerItems.hasItem(TrainerItemId.MEGA_BRACELET)) && + ((fc.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) === -1 && + fc.formKey.indexOf(SpeciesFormKey.ETERNAMAX) === -1) || + globalScene.trainerItems.hasItem(TrainerItemId.DYNAMAX_BAND)) && + (!fc.conditions.length || + fc.conditions.filter(cond => cond instanceof SpeciesFormChangeCondition && cond.predicate(p)) + .length) && + fc.preFormKey === p.getFormKey(), + ) + .map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) + .filter(t => t?.active && !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... + let foundULTRA_Z = false, + foundN_LUNA = false, + foundN_SOLAR = false; + formChangeItemTriggers.forEach((fc, _i) => { + console.log("Checking ", fc.item); + switch (fc.item) { + case FormChangeItem.ULTRANECROZIUM_Z: + foundULTRA_Z = true; + break; + case FormChangeItem.N_LUNARIZER: + foundN_LUNA = true; + break; + case FormChangeItem.N_SOLARIZER: + foundN_SOLAR = true; + break; + } + }); + if (foundULTRA_Z && foundN_LUNA && foundN_SOLAR) { + // all three items are present -> user hasn't acquired any of the N_*ARIZERs -> block ULTRANECROZIUM_Z acquisition. + formChangeItemTriggers = formChangeItemTriggers.filter( + fc => fc.item !== FormChangeItem.ULTRANECROZIUM_Z, + ); + } else { + console.log("DID NOT FIND "); + } + } + return formChangeItemTriggers; + }), + ), + ] + .flat() + .flatMap(fc => fc.item) + .filter(i => (i && i < 100) === isRareFormChangeItem); + // convert it into a set to remove duplicate values, which can appear when the same species with a potential form change is in the party. + + if (!formChangeItemPool.length) { + return null; + } + + // TODO: should this use `randSeedItem`? + return new FormChangeItemReward(formChangeItemPool[randSeedInt(formChangeItemPool.length)]); + }); + this.id = id; + } +} + +export class RewardOption { + public type: Reward; + public upgradeCount: number; + public tier: RarityTier; + public cost: number; + + constructor(type: Reward, upgradeCount: number, tier: RarityTier, cost = 0) { + this.type = type; + this.upgradeCount = upgradeCount; + this.tier = tier; + this.cost = Math.min(Math.round(cost), Number.MAX_SAFE_INTEGER); + } +} + +// TODO: If necessary, add the rest of the modifier types here. +// For now, doing the minimal work until the modifier rework lands. +const RewardConstructorMap = Object.freeze({ + RewardGenerator, +}); + +/** + * Map of of modifier type strings to their constructor type + */ +export type RewardConstructorMap = typeof RewardConstructorMap; + +/** + * Map of modifier type strings to their instance type + */ +export type RewardInstanceMap = { + [K in keyof RewardConstructorMap]: InstanceType; +}; + +export type RewardString = keyof RewardConstructorMap; diff --git a/src/items/trainer-item-data-types.ts b/src/items/trainer-item-data-types.ts new file mode 100644 index 00000000000..305b3f1ebb3 --- /dev/null +++ b/src/items/trainer-item-data-types.ts @@ -0,0 +1,45 @@ +// TODO: move to `src/@types/` +import type { RarityTier } from "#enums/reward-tier"; +import type { TrainerItemId } from "#enums/trainer-item-id"; + +export type TrainerItemData = { + stack: number; + disabled?: boolean; + cooldown?: number; +}; + +export type TrainerItemDataMap = { + [key in TrainerItemId]?: TrainerItemData; +}; + +export type TrainerItemSpecs = TrainerItemData & { + id: TrainerItemId; +}; + +export function isTrainerItemSpecs(entry: any): entry is TrainerItemSpecs { + return typeof entry.id === "number" && "stack" in entry; +} + +type TrainerItemPoolEntry = { + entry: TrainerItemId; + weight: number; +}; + +export type TrainerItemPool = TrainerItemPoolEntry[]; + +export type TrainerItemTieredPool = { + [key in RarityTier]?: TrainerItemPool; +}; + +export function isTrainerItemPool(value: any): value is TrainerItemPool { + return Array.isArray(value) && value.every(entry => "entry" in entry && "weight" in entry); +} + +type TrainerItemConfigurationEntry = { + entry: TrainerItemId | TrainerItemSpecs; + count?: number | (() => number); +}; + +export type TrainerItemConfiguration = TrainerItemConfigurationEntry[]; + +export type TrainerItemSaveData = TrainerItemSpecs[]; diff --git a/src/items/trainer-item-default-tiers.ts b/src/items/trainer-item-default-tiers.ts new file mode 100644 index 00000000000..aab1d0a6f02 --- /dev/null +++ b/src/items/trainer-item-default-tiers.ts @@ -0,0 +1,50 @@ +import { RarityTier } from "#enums/reward-tier"; +import { TrainerItemId } from "#enums/trainer-item-id"; + +export const trainerItemRarities = { + [TrainerItemId.MAP]: RarityTier.COMMON, + [TrainerItemId.IV_SCANNER]: RarityTier.ULTRA, + [TrainerItemId.LOCK_CAPSULE]: RarityTier.ROGUE, + [TrainerItemId.MEGA_BRACELET]: RarityTier.ROGUE, + [TrainerItemId.DYNAMAX_BAND]: RarityTier.ROGUE, + [TrainerItemId.TERA_ORB]: RarityTier.ULTRA, + + [TrainerItemId.GOLDEN_POKEBALL]: RarityTier.LUXURY, + + [TrainerItemId.OVAL_CHARM]: RarityTier.LUXURY, + [TrainerItemId.EXP_SHARE]: RarityTier.ULTRA, + [TrainerItemId.EXP_BALANCE]: RarityTier.LUXURY, + + [TrainerItemId.CANDY_JAR]: RarityTier.ULTRA, + [TrainerItemId.BERRY_POUCH]: RarityTier.ROGUE, + + [TrainerItemId.HEALING_CHARM]: RarityTier.MASTER, + [TrainerItemId.EXP_CHARM]: RarityTier.ULTRA, + [TrainerItemId.SUPER_EXP_CHARM]: RarityTier.ROGUE, + [TrainerItemId.GOLDEN_EXP_CHARM]: RarityTier.LUXURY, + [TrainerItemId.AMULET_COIN]: RarityTier.ULTRA, + + [TrainerItemId.ABILITY_CHARM]: RarityTier.ULTRA, + [TrainerItemId.SHINY_CHARM]: RarityTier.MASTER, + [TrainerItemId.CATCHING_CHARM]: RarityTier.ULTRA, + + [TrainerItemId.BLACK_SLUDGE]: RarityTier.LUXURY, + [TrainerItemId.GOLDEN_BUG_NET]: RarityTier.LUXURY, + + [TrainerItemId.LURE]: RarityTier.COMMON, + [TrainerItemId.SUPER_LURE]: RarityTier.GREAT, + [TrainerItemId.MAX_LURE]: RarityTier.ULTRA, + + [TrainerItemId.X_ATTACK]: RarityTier.COMMON, + [TrainerItemId.X_DEFENSE]: RarityTier.COMMON, + [TrainerItemId.X_SP_ATK]: RarityTier.COMMON, + [TrainerItemId.X_SP_DEF]: RarityTier.COMMON, + [TrainerItemId.X_SPEED]: RarityTier.COMMON, + [TrainerItemId.X_ACCURACY]: RarityTier.COMMON, + [TrainerItemId.DIRE_HIT]: RarityTier.GREAT, +}; + +export function getTrainerItemTier(item: TrainerItemId): RarityTier { + const tier = trainerItemRarities[item]; + return tier ?? RarityTier.LUXURY; +} diff --git a/src/items/trainer-item-manager.ts b/src/items/trainer-item-manager.ts new file mode 100644 index 00000000000..6579df69ca5 --- /dev/null +++ b/src/items/trainer-item-manager.ts @@ -0,0 +1,157 @@ +import { allTrainerItems } from "#data/data-lists"; +import type { TrainerItemId } from "#enums/trainer-item-id"; +import { + isTrainerItemSpecs, + type TrainerItemConfiguration, + type TrainerItemDataMap, + type TrainerItemSaveData, + type TrainerItemSpecs, +} from "#items/trainer-item-data-types"; +import { getTypedEntries, getTypedKeys } from "#utils/common"; + +export class TrainerItemManager { + public trainerItems: TrainerItemDataMap; + + constructor() { + this.trainerItems = {}; + } + + getItemSpecs(id: TrainerItemId): TrainerItemSpecs | undefined { + const item = this.trainerItems[id]; + if (item) { + const itemSpecs: TrainerItemSpecs = { + ...item, + id, + }; + return itemSpecs; + } + return undefined; + } + + generateTrainerItemConfiguration(restrictedIds?: TrainerItemId[]): TrainerItemConfiguration { + const config: TrainerItemConfiguration = []; + for (const [id, item] of getTypedEntries(this.trainerItems)) { + if (item && (!restrictedIds || id in restrictedIds)) { + const specs: TrainerItemSpecs = { ...item, id }; + config.push({ entry: specs, count: 1 }); + } + } + return config; + } + + generateSaveData(): TrainerItemSaveData { + const saveData: TrainerItemSaveData = []; + for (const [id, item] of getTypedEntries(this.trainerItems)) { + if (item) { + const specs: TrainerItemSpecs = { ...item, id }; + saveData.push(specs); + } + } + return saveData; + } + + getTrainerItems(): TrainerItemId[] { + return getTypedKeys(this.trainerItems); + } + + hasItem(itemType: TrainerItemId): boolean { + return itemType in this.trainerItems; + } + + getStack(itemType: TrainerItemId): number { + const item = this.trainerItems[itemType]; + return item ? item.stack : 0; + } + + isMaxStack(itemType: TrainerItemId): boolean { + const item = this.trainerItems[itemType]; + return item ? item.stack >= allTrainerItems[itemType].getMaxStackCount() : false; + } + + overrideItems(newItems: TrainerItemDataMap) { + this.trainerItems = newItems; + // The following is to allow randomly generated item configs to have stack 0 + for (const [item, properties] of Object.entries(this.trainerItems)) { + if (!properties || properties.stack <= 0) { + delete this.trainerItems[item]; + } + } + } + + add(itemType: TrainerItemId | TrainerItemSpecs, addStack?: number): boolean { + if (isTrainerItemSpecs(itemType)) { + return this.addItemWithSpecs(itemType); + } + + if (!addStack) { + addStack = allTrainerItems[itemType].isLapsing ? allTrainerItems[itemType].getMaxStackCount() : 1; + } + const maxStack = allTrainerItems[itemType].getMaxStackCount(); + const item = this.trainerItems[itemType]; + + if (item) { + // TODO: We may want an error message of some kind instead + if (item.stack < maxStack) { + item.stack = Math.min(item.stack + addStack, maxStack); + return true; + } + } else { + this.trainerItems[itemType] = { stack: Math.min(addStack, maxStack) }; + return true; + } + return false; + } + + addItemWithSpecs(itemSpecs: TrainerItemSpecs): boolean { + const id = itemSpecs.id; + const maxStack = allTrainerItems[id].getMaxStackCount(); + const item = this.trainerItems[id]; + + const tempStack = item?.stack ?? 0; + + this.trainerItems[id] = itemSpecs; + this.trainerItems[id].stack = Math.min(itemSpecs.stack + tempStack, maxStack); + + return true; + } + + remove(itemType: TrainerItemId, removeStack = 1, all = false) { + const item = this.trainerItems[itemType]; + + if (item) { + item.stack -= removeStack; + + if (all || item.stack <= 0) { + delete this.trainerItems[itemType]; + } + } + } + + filterRequestedItems(requestedItems: TrainerItemId[], exclude = false) { + const currentItems = this.getTrainerItems(); + return currentItems.filter(it => !exclude && requestedItems.some(entry => it === entry)); + } + + getTrainerItemCount(): number { + let total = 0; + for (const properties of Object.values(this.trainerItems)) { + total += properties?.stack ?? 0; + } + return total; + } + + lapseItems(): void { + for (const [item, properties] of Object.entries(this.trainerItems)) { + if (allTrainerItems[item].isLapsing && properties) { + properties.stack -= 1; + } + if (!properties || properties.stack <= 0) { + delete this.trainerItems[item]; + } + } + } + + clearItems() { + this.trainerItems = {}; + } +} diff --git a/src/items/trainer-item-pool.ts b/src/items/trainer-item-pool.ts new file mode 100644 index 00000000000..e75f1cec8e1 --- /dev/null +++ b/src/items/trainer-item-pool.ts @@ -0,0 +1,62 @@ +import { globalScene } from "#app/global-scene"; +import { allTrainerItems } from "#data/data-lists"; +import { RarityTier } from "#enums/reward-tier"; +import type { TrainerItemId } from "#enums/trainer-item-id"; +import type { TrainerItemPool, TrainerItemTieredPool } from "#items/trainer-item-data-types"; +import type { TrainerItemManager } from "#items/trainer-item-manager"; +import { isNullOrUndefined, pickWeightedIndex } from "#utils/common"; + +export const enemyBuffTokenPool: TrainerItemTieredPool = {}; + +function getPoolWeights(pool: TrainerItemPool, manager: TrainerItemManager): number[] { + return pool.map(p => { + if (manager.isMaxStack(p.entry)) { + return 0; + } + return p.weight; + }); +} + +export function getNewTrainerItemFromPool(pool: TrainerItemPool, manager: TrainerItemManager): TrainerItemId { + const weights = getPoolWeights(pool, manager); + + const pickedIndex = pickWeightedIndex(weights); + if (isNullOrUndefined(pickedIndex)) { + return 0; + } + const entry = pool[pickedIndex].entry; + + return entry as TrainerItemId; +} + +export function assignEnemyBuffTokenForWave(tier: RarityTier) { + let tierStackCount: number; + switch (tier) { + case RarityTier.ULTRA: + tierStackCount = 5; + break; + case RarityTier.GREAT: + tierStackCount = 3; + break; + default: + tierStackCount = 1; + break; + } + + if (!enemyBuffTokenPool[tier]) { + return; + } + + const retryCount = 50; + let candidate = getNewTrainerItemFromPool(enemyBuffTokenPool[tier], globalScene.enemyTrainerItems); + let r = 0; + while ( + ++r < retryCount && + allTrainerItems[candidate].getMaxStackCount() < + globalScene.enemyTrainerItems.getStack(candidate) + (r < 10 ? tierStackCount : 1) + ) { + candidate = getNewTrainerItemFromPool(enemyBuffTokenPool[tier], globalScene.enemyTrainerItems); + } + + globalScene.enemyTrainerItems.add(candidate, tierStackCount); +} diff --git a/src/items/trainer-item.ts b/src/items/trainer-item.ts new file mode 100644 index 00000000000..b2a6c15389c --- /dev/null +++ b/src/items/trainer-item.ts @@ -0,0 +1,575 @@ +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { getStatusEffectDescriptor, getStatusEffectHealText } from "#data/status-effect"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { getStatKey, Stat, type TempBattleStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { TrainerItemId, TrainerItemNames } from "#enums/trainer-item-id"; +import type { Pokemon } from "#field/pokemon"; +import type { TrainerItemManager } from "#items/trainer-item-manager"; +import { addTextObject, TextStyle } from "#ui/text"; +import { type BooleanHolder, hslToHex, type NumberHolder, randSeedFloat, toDmgValue } from "#utils/common"; +import i18next from "i18next"; + +export const TrainerItemEffect = { + LEVEL_INCREMENT_BOOSTER: 1, + PRESERVE_BERRY: 2, + HEALING_BOOSTER: 3, + EXP_BOOSTER: 4, + MONEY_MULTIPLIER: 5, + HIDDEN_ABILITY_CHANCE_BOOSTER: 6, + SHINY_RATE_BOOSTER: 7, + CRITICAL_CATCH_CHANCE_BOOSTER: 8, + EXTRA_REWARD: 9, + + HEAL_SHOP_COST: 10, + DOUBLE_BATTLE_CHANCE_BOOSTER: 11, + + TEMP_STAT_STAGE_BOOSTER: 12, + TEMP_ACCURACY_BOOSTER: 13, + TEMP_CRIT_BOOSTER: 14, + + ENEMY_DAMAGE_BOOSTER: 15, + ENEMY_DAMAGE_REDUCER: 16, + ENEMY_HEAL: 17, + ENEMY_ATTACK_STATUS_CHANCE: 18, + ENEMY_STATUS_HEAL_CHANCE: 19, + ENEMY_ENDURE_CHANCE: 20, + ENEMY_FUSED_CHANCE: 21, +} as const; + +export type TrainerItemEffect = (typeof TrainerItemEffect)[keyof typeof TrainerItemEffect]; + +export interface NumberHolderParams { + numberHolder: NumberHolder; +} + +export interface BooleanHolderParams { + booleanHolder: BooleanHolder; +} + +export interface PokemonParams { + pokemon: Pokemon; +} + +export class TrainerItem { + // public pokemonId: number; + public type: TrainerItemId; + public maxStackCount: number; + public isLapsing = false; + public effects: TrainerItemEffect[] = []; + + //TODO: If this is actually never changed by any subclass, perhaps it should not be here + public soundName = "se/restore"; + + constructor(type: TrainerItemId, maxStackCount = 1) { + this.type = type; + this.maxStackCount = maxStackCount; + } + + public get name(): string { + return i18next.t(`modifierType:ModifierType.${TrainerItemNames[this.type]}.name`); + } + + public get description(): string { + return i18next.t(`modifierType:ModifierType.${TrainerItemNames[this.type]}.description`); + } + + public get iconName(): string { + return `${TrainerItemNames[this.type]?.toLowerCase()}`; + } + + getMaxStackCount(): number { + return this.maxStackCount; + } + + createIcon(stackCount: number): Phaser.GameObjects.Container { + const container = globalScene.add.container(); + + container.add(globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5)); + + const stackText = this.getIconStackText(stackCount); + if (stackText) { + container.add(stackText); + } + + return container; + } + + getIconStackText(stackCount: number): Phaser.GameObjects.BitmapText | null { + if (this.getMaxStackCount() === 1 || stackCount < 1) { + return null; + } + + const text = globalScene.add + .bitmapText(10, 15, "item-count", stackCount.toString(), 11) + .setLetterSpacing(-0.5) + .setOrigin(0); + if (stackCount >= this.getMaxStackCount()) { + text.setTint(0xf89890); + } + + return text; + } + + getScoreMultiplier(): number { + return 1; + } +} + +// Candy Jar +export class LevelIncrementBoosterTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.LEVEL_INCREMENT_BOOSTER]; + + apply(manager: TrainerItemManager, params: NumberHolderParams) { + const count = params.numberHolder; + const stack = manager.getStack(this.type); + count.value += stack; + } +} + +// Berry Pouch +export interface PreserveBerryParams { + pokemon: Pokemon; + doPreserve: BooleanHolder; +} + +export class PreserveBerryTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.PRESERVE_BERRY]; + + apply(manager: TrainerItemManager, params: PreserveBerryParams) { + const stack = manager.getStack(this.type); + params.doPreserve.value ||= params.pokemon.randBattleSeedInt(10) < stack * 3; + } +} + +// Healing Charm +export class HealingBoosterTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.HEALING_BOOSTER]; + private multiplier: number; + + constructor(type: TrainerItemId, multiplier: number, stackCount?: number) { + super(type, stackCount); + + this.multiplier = multiplier; + } + + apply(manager: TrainerItemManager, params: NumberHolderParams) { + const healingMultiplier = params.numberHolder; + const stack = manager.getStack(this.type); + healingMultiplier.value *= 1 + (this.multiplier - 1) * stack; + } +} + +// Exp Booster +export class ExpBoosterTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.EXP_BOOSTER]; + private boostPercent: number; + + constructor(type: TrainerItemId, boostPercent: number, stackCount?: number) { + super(type, stackCount); + + this.boostPercent = boostPercent; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.ExpBoosterModifierType.description", { + boostPercent: this.boostPercent, + }); + } + + apply(manager: TrainerItemManager, params: NumberHolderParams) { + const boost = params.numberHolder; + const stack = manager.getStack(this.type); + boost.value = Math.floor(boost.value * (1 + stack * this.boostPercent * 0.01)); + } +} + +export class MoneyMultiplierTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.MONEY_MULTIPLIER]; + + apply(manager: TrainerItemManager, params: NumberHolderParams) { + const moneyMultiplier = params.numberHolder; + const stack = manager.getStack(this.type); + moneyMultiplier.value += Math.floor(moneyMultiplier.value * 0.2 * stack); + } +} + +export class HiddenAbilityChanceBoosterTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER]; + + apply(manager: TrainerItemManager, params: NumberHolderParams) { + const boost = params.numberHolder; + const stack = manager.getStack(this.type); + boost.value *= Math.pow(2, -1 - stack); + } +} + +export class ShinyRateBoosterTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.SHINY_RATE_BOOSTER]; + + apply(manager: TrainerItemManager, params: NumberHolderParams) { + const boost = params.numberHolder; + const stack = manager.getStack(this.type); + boost.value *= Math.pow(2, 1 + stack); + } +} + +export class CriticalCatchChanceBoosterTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.CRITICAL_CATCH_CHANCE_BOOSTER]; + + apply(manager: TrainerItemManager, params: NumberHolderParams) { + const boost = params.numberHolder; + const stack = manager.getStack(this.type); + boost.value *= 1.5 + stack / 2; + } +} + +export class ExtraRewardTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.EXTRA_REWARD]; + + apply(manager: TrainerItemManager, params: NumberHolderParams) { + const count = params.numberHolder; + const stack = manager.getStack(this.type); + count.value += stack; + } +} + +export class HealShopCostTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.HEAL_SHOP_COST]; + public readonly shopMultiplier: number; + + constructor(type: TrainerItemId, shopMultiplier: number, stackCount?: number) { + super(type, stackCount); + + this.shopMultiplier = shopMultiplier; + } + + apply(_manager: TrainerItemManager, params: NumberHolderParams) { + const moneyCost = params.numberHolder; + moneyCost.value = Math.floor(moneyCost.value * this.shopMultiplier); + } +} + +export class LapsingTrainerItem extends TrainerItem { + isLapsing = true; + + createIcon(battleCount: number): Phaser.GameObjects.Container { + const container = globalScene.add.container(0, 0); + + const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5); + container.add(item); + + // Linear interpolation on hue + const hue = Math.floor(120 * (battleCount / this.getMaxStackCount()) + 5); + + // Generates the color hex code with a constant saturation and lightness but varying hue + const typeHex = hslToHex(hue, 0.5, 0.9); + const strokeHex = hslToHex(hue, 0.7, 0.3); + + const battleCountText = addTextObject(27, 0, battleCount.toString(), TextStyle.PARTY, { + fontSize: "66px", + color: typeHex, + }); + battleCountText.setShadow(0, 0); + battleCountText.setStroke(strokeHex, 16); + battleCountText.setOrigin(1, 0); + container.add(battleCountText); + + return container; + } +} + +export class DoubleBattleChanceBoosterTrainerItem extends LapsingTrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.DOUBLE_BATTLE_CHANCE_BOOSTER]; + + get description(): string { + return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", { + battleCount: this.getMaxStackCount(), + }); + } + + apply(_manager: TrainerItemManager, params: NumberHolderParams) { + const doubleBattleChance = params.numberHolder; + // This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using randSeedInt + // A double battle will initiate if the generated number is 0 + doubleBattleChance.value /= 4; + } +} + +type TempStatToTrainerItemMap = { + [key in TempBattleStat]: TrainerItemId; +}; + +export const tempStatToTrainerItem: TempStatToTrainerItemMap = { + [Stat.ATK]: TrainerItemId.X_ATTACK, + [Stat.DEF]: TrainerItemId.X_DEFENSE, + [Stat.SPATK]: TrainerItemId.X_SP_ATK, + [Stat.SPDEF]: TrainerItemId.X_SP_DEF, + [Stat.SPD]: TrainerItemId.X_SPEED, + [Stat.ACC]: TrainerItemId.X_ACCURACY, +}; + +export class TempStatStageBoosterTrainerItem extends LapsingTrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.TEMP_STAT_STAGE_BOOSTER]; + private stat: TempBattleStat; + + constructor(type: TrainerItemId, stat: TempBattleStat, stackCount?: number) { + super(type, stackCount); + + this.stat = stat; + } + + get name(): string { + return i18next.t(`modifierType:TempStatStageBoosterItem.${TrainerItemNames[this.type]?.toLowerCase()}`); + } + + get description(): string { + console.log(); + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { + stat: i18next.t(getStatKey(this.stat)), + amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.percentage"), + }); + } + + apply(_manager: TrainerItemManager, params: NumberHolderParams) { + const statLevel = params.numberHolder; + const boost = 0.3; + statLevel.value += boost; + } +} + +export class TempAccuracyBoosterTrainerItem extends LapsingTrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.TEMP_ACCURACY_BOOSTER]; + + get name(): string { + return i18next.t(`modifierType:TempStatStageBoosterItem.${TrainerItemNames[this.type]?.toLowerCase()}`); + } + + get description(): string { + console.log(); + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { + stat: i18next.t(getStatKey(Stat.ACC)), + amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.percentage"), + }); + } + + apply(_manager: TrainerItemManager, params: NumberHolderParams) { + const statLevel = params.numberHolder; + const boost = 1; + statLevel.value += boost; + } +} + +export class TempCritBoosterTrainerItem extends LapsingTrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.TEMP_CRIT_BOOSTER]; + + get description(): string { + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { + stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises"), + amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.stage"), + }); + } + + apply(_manager: TrainerItemManager, params: NumberHolderParams) { + const critLevel = params.numberHolder; + critLevel.value++; + } +} + +export class EnemyDamageBoosterTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.ENEMY_DAMAGE_BOOSTER]; + public damageBoost = 1.05; + + get iconName(): string { + return "wl_item_drop"; + } + + apply(manager: TrainerItemManager, params: NumberHolderParams): boolean { + const stack = manager.getStack(this.type); + const multiplier = params.numberHolder; + + multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageBoost, stack)); + + return true; + } + + getMaxStackCount(): number { + return 999; + } +} + +export class EnemyDamageReducerTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.ENEMY_DAMAGE_REDUCER]; + public damageReduction = 0.975; + + get iconName(): string { + return "wl_guard_spec"; + } + + apply(manager: TrainerItemManager, params: NumberHolderParams): boolean { + const stack = manager.getStack(this.type); + const multiplier = params.numberHolder; + + multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageReduction, stack)); + + return true; + } + + getMaxStackCount(): number { + return globalScene.currentBattle.waveIndex < 2000 ? 99 : 999; + } +} + +export class EnemyTurnHealTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.ENEMY_HEAL]; + public healPercent = 2; + + get iconName(): string { + return "wl_potion"; + } + + apply(manager: TrainerItemManager, params: PokemonParams): boolean { + const stack = manager.getStack(this.type); + const enemyPokemon = params.pokemon; + + if (!enemyPokemon.isFullHp()) { + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + enemyPokemon.getBattlerIndex(), + Math.max(Math.floor(enemyPokemon.getMaxHp() / (100 / this.healPercent)) * stack, 1), + i18next.t("modifier:enemyTurnHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), + }), + true, + false, + false, + false, + true, + ); + return true; + } + + return false; + } +} + +export class EnemyAttackStatusEffectChanceTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.ENEMY_ATTACK_STATUS_CHANCE]; + public effect: StatusEffect; + + constructor(type: TrainerItemId, effect: StatusEffect, stackCount?: number) { + super(type, stackCount); + + this.effect = effect; + } + + get iconName(): string { + if (this.effect === StatusEffect.POISON) { + return "wl_antidote"; + } + if (this.effect === StatusEffect.PARALYSIS) { + return "wl_paralyze_heal"; + } + if (this.effect === StatusEffect.BURN) { + return "wl_burn_heal"; + } + return ""; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.EnemyAttackStatusEffectChanceModifierType.description", { + chancePercent: this.getChance() * 100, + statusEffect: getStatusEffectDescriptor(this.effect), + }); + } + + apply(manager: TrainerItemManager, params: PokemonParams): boolean { + const stack = manager.getStack(this.type); + const enemyPokemon = params.pokemon; + const chance = this.getChance(); + + if (randSeedFloat() <= chance * stack) { + return enemyPokemon.trySetStatus(this.effect, true); + } + + return false; + } + + getChance(): number { + return 0.025 * (this.effect === StatusEffect.BURN || this.effect === StatusEffect.POISON ? 2 : 1); + } +} + +export class EnemyStatusEffectHealChanceTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.ENEMY_STATUS_HEAL_CHANCE]; + public chance = 0.025; + + get iconName(): string { + return "wl_full_heal"; + } + + apply(manager: TrainerItemManager, params: PokemonParams): boolean { + const stack = manager.getStack(this.type); + const enemyPokemon = params.pokemon; + + if (!enemyPokemon.status || randSeedFloat() > this.chance * stack) { + return false; + } + + globalScene.phaseManager.queueMessage( + getStatusEffectHealText(enemyPokemon.status.effect, getPokemonNameWithAffix(enemyPokemon)), + ); + enemyPokemon.resetStatus(); + enemyPokemon.updateInfo(); + return true; + } +} + +export class EnemyEndureChanceTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.ENEMY_ENDURE_CHANCE]; + public chance = 2; + + get iconName(): string { + return "wl_reset_urge"; + } + + get description(): string { + return i18next.t("modifierType:ModifierType.EnemyEndureChanceModifierType.description", { + chancePercent: this.chance, + }); + } + + apply(manager: TrainerItemManager, params: PokemonParams): boolean { + const stack = manager.getStack(this.type); + const target = params.pokemon; + + if (target.waveData.endured || target.randBattleSeedInt(100) >= this.chance * stack) { + return false; + } + + target.addTag(BattlerTagType.ENDURE_TOKEN, 1); + + target.waveData.endured = true; + + return true; + } +} + +export class EnemyFusionChanceTrainerItem extends TrainerItem { + public effects: TrainerItemEffect[] = [TrainerItemEffect.ENEMY_FUSED_CHANCE]; + public chance = 0.01; + + get iconName(): string { + return "wl_custom_spliced"; + } + + apply(manager: TrainerItemManager, params: BooleanHolderParams) { + const stack = manager.getStack(this.type); + const isFusion = params.booleanHolder; + if (randSeedFloat() > this.chance * stack) { + return false; + } + isFusion.value = true; + } +} diff --git a/src/modifier/init-modifier-pools.ts b/src/modifier/init-modifier-pools.ts deleted file mode 100644 index 316d4dae741..00000000000 --- a/src/modifier/init-modifier-pools.ts +++ /dev/null @@ -1,860 +0,0 @@ -/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */ -import type { initModifierTypes } from "#modifiers/modifier-type"; -/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */ - -import { timedEventManager } from "#app/global-event-manager"; -import { globalScene } from "#app/global-scene"; -import { pokemonEvolutions } from "#balance/pokemon-evolutions"; -import { modifierTypes } from "#data/data-lists"; -import { MAX_PER_TYPE_POKEBALLS } from "#data/pokeball"; -import { AbilityId } from "#enums/ability-id"; -import { BerryType } from "#enums/berry-type"; -import { ModifierTier } from "#enums/modifier-tier"; -import { MoveId } from "#enums/move-id"; -import { PokeballType } from "#enums/pokeball"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { Unlockables } from "#enums/unlockables"; -import type { Pokemon } from "#field/pokemon"; -import { - BerryModifier, - DoubleBattleChanceBoosterModifier, - SpeciesCritBoosterModifier, - TurnStatusEffectModifier, -} from "#modifiers/modifier"; -import { - dailyStarterModifierPool, - enemyBuffModifierPool, - modifierPool, - trainerModifierPool, - wildModifierPool, -} from "#modifiers/modifier-pools"; -import { WeightedModifierType } from "#modifiers/modifier-type"; -import type { WeightedModifierTypeWeightFunc } from "#types/modifier-types"; -import { isNullOrUndefined } from "#utils/common"; - -/** - * 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; - }); -} - -/** - * Initialize the common modifier pool - */ -function initCommonModifierPool() { - modifierPool[ModifierTier.COMMON] = [ - new WeightedModifierType(modifierTypes.POKEBALL, () => (hasMaximumBalls(PokeballType.POKEBALL) ? 0 : 6), 6), - new WeightedModifierType(modifierTypes.RARE_CANDY, 2), - new WeightedModifierType( - modifierTypes.POTION, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter(p => p.getInverseHp() >= 10 && p.getHpRatio() <= 0.875 && !p.isFainted()).length, - 3, - ); - return thresholdPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.SUPER_POTION, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter(p => p.getInverseHp() >= 25 && p.getHpRatio() <= 0.75 && !p.isFainted()).length, - 3, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType( - modifierTypes.ETHER, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && - p - .getMoveset() - .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) - .length, - ).length, - 3, - ); - return thresholdPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.MAX_ETHER, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && - p - .getMoveset() - .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) - .length, - ).length, - 3, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), - new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4), - new WeightedModifierType(modifierTypes.BERRY, 2), - new WeightedModifierType(modifierTypes.TM_COMMON, 2), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }); -} - -/** - * Initialize the Great modifier pool - */ -function initGreatModifierPool() { - modifierPool[ModifierTier.GREAT] = [ - new WeightedModifierType(modifierTypes.GREAT_BALL, () => (hasMaximumBalls(PokeballType.GREAT_BALL) ? 0 : 6), 6), - new WeightedModifierType(modifierTypes.PP_UP, 2), - new WeightedModifierType( - modifierTypes.FULL_HEAL, - (party: Pokemon[]) => { - const statusEffectPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !!p.status && - !p.getHeldItems().some(i => { - if (i instanceof TurnStatusEffectModifier) { - return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; - } - return false; - }), - ).length, - 3, - ); - return statusEffectPartyMemberCount * 6; - }, - 18, - ), - new WeightedModifierType( - modifierTypes.REVIVE, - (party: Pokemon[]) => { - const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); - return faintedPartyMemberCount * 9; - }, - 27, - ), - new WeightedModifierType( - modifierTypes.MAX_REVIVE, - (party: Pokemon[]) => { - const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); - return faintedPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.SACRED_ASH, - (party: Pokemon[]) => { - return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; - }, - 1, - ), - new WeightedModifierType( - modifierTypes.HYPER_POTION, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.625 && !p.isFainted()).length, - 3, - ); - return thresholdPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.MAX_POTION, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5 && !p.isFainted()).length, - 3, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType( - modifierTypes.FULL_RESTORE, - (party: Pokemon[]) => { - const statusEffectPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !!p.status && - !p.getHeldItems().some(i => { - if (i instanceof TurnStatusEffectModifier) { - return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; - } - return false; - }), - ).length, - 3, - ); - const thresholdPartyMemberCount = Math.floor( - (Math.min(party.filter(p => p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5 && !p.isFainted()).length, 3) + - statusEffectPartyMemberCount) / - 2, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType( - modifierTypes.ELIXIR, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && - p - .getMoveset() - .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) - .length, - ).length, - 3, - ); - return thresholdPartyMemberCount * 3; - }, - 9, - ), - new WeightedModifierType( - modifierTypes.MAX_ELIXIR, - (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min( - party.filter( - p => - p.hp && - !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && - p - .getMoveset() - .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) - .length, - ).length, - 3, - ); - return thresholdPartyMemberCount; - }, - 3, - ), - new WeightedModifierType(modifierTypes.DIRE_HIT, 4), - new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(15, 4)), - new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)), - new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 4), - new WeightedModifierType( - modifierTypes.EVOLUTION_ITEM, - () => { - return Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 15), 8); - }, - 8, - ), - new WeightedModifierType( - modifierTypes.MAP, - () => (globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex < 180 ? 2 : 0), - 2, - ), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, 2), - new WeightedModifierType(modifierTypes.TM_GREAT, 3), - new WeightedModifierType( - modifierTypes.MEMORY_MUSHROOM, - (party: Pokemon[]) => { - if (!party.find(p => p.getLearnableLevelMoves().length)) { - return 0; - } - const highestPartyLevel = party - .map(p => p.level) - .reduce((highestLevel: number, level: number) => Math.max(highestLevel, level), 1); - return Math.min(Math.ceil(highestPartyLevel / 20), 4); - }, - 4, - ), - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), - new WeightedModifierType(modifierTypes.TERA_SHARD, (party: Pokemon[]) => - party.filter( - p => - !(p.hasSpecies(SpeciesId.TERAPAGOS) || p.hasSpecies(SpeciesId.OGERPON) || p.hasSpecies(SpeciesId.SHEDINJA)), - ).length > 0 - ? 1 - : 0, - ), - new WeightedModifierType( - modifierTypes.DNA_SPLICERS, - (party: Pokemon[]) => { - if (party.filter(p => !p.fusionSpecies).length > 1) { - if (globalScene.gameMode.isSplicedOnly) { - return 4; - } - if (globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) { - return 2; - } - } - return 0; - }, - 4, - ), - new WeightedModifierType( - modifierTypes.VOUCHER, - (_party: Pokemon[], rerollCount: number) => (!globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0), - 1, - ), - ].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }); -} - -/** - * Initialize the Ultra modifier pool - */ -function initUltraModifierPool() { - modifierPool[ModifierTier.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)), - new WeightedModifierType(modifierTypes.PP_MAX, 3), - new WeightedModifierType(modifierTypes.MINT, 4), - new WeightedModifierType( - modifierTypes.RARE_EVOLUTION_ITEM, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 15) * 4, 32), - 32, - ), - new WeightedModifierType( - modifierTypes.FORM_CHANGE_ITEM, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 6, - 24, - ), - new WeightedModifierType(modifierTypes.AMULET_COIN, skipInLastClassicWaveOrDefault(3)), - new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => { - const { gameMode, gameData } = globalScene; - if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) { - return party.some(p => { - // Check if Pokemon's species (or fusion species, if applicable) can evolve or if they're G-Max'd - if ( - !p.isMax() && - (p.getSpeciesForm(true).speciesId in pokemonEvolutions || - (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 false; - }) - ? 10 - : 0; - } - return 0; - }), - new WeightedModifierType(modifierTypes.RARE_SPECIES_STAT_BOOSTER, 12), - new WeightedModifierType( - modifierTypes.LEEK, - (party: Pokemon[]) => { - const checkedSpecies = [SpeciesId.FARFETCHD, SpeciesId.GALAR_FARFETCHD, SpeciesId.SIRFETCHD]; - // 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) && - (checkedSpecies.includes(p.getSpeciesForm(true).speciesId) || - (p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId))), - ) - ? 12 - : 0; - }, - 12, - ), - new WeightedModifierType( - 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"); - - if (!isHoldingOrb) { - const moveset = p - .getMoveset(true) - .filter(m => !isNullOrUndefined(m)) - .map(m => m.moveId); - const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); - - // Moves that take advantage of obtaining the actual status effect - const hasStatusMoves = [MoveId.FACADE, MoveId.PSYCHO_SHIFT].some(m => moveset.includes(m)); - // Moves that take advantage of being able to give the target a status orb - // TODO: Take moves (Trick, Fling, Switcheroo) from comment when they are implemented - const hasItemMoves = [ - /* MoveId.TRICK, MoveId.FLING, MoveId.SWITCHEROO */ - ].some(m => moveset.includes(m)); - - if (canSetStatus) { - // Abilities that take advantage of obtaining the actual status effect, separated based on specificity to the orb - const hasGeneralAbility = [ - AbilityId.QUICK_FEET, - AbilityId.GUTS, - AbilityId.MARVEL_SCALE, - AbilityId.MAGIC_GUARD, - ].some(a => p.hasAbility(a, false, true)); - const hasSpecificAbility = [AbilityId.TOXIC_BOOST, AbilityId.POISON_HEAL].some(a => - p.hasAbility(a, false, true), - ); - const hasOppositeAbility = [AbilityId.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); - - return hasSpecificAbility || (hasGeneralAbility && !hasOppositeAbility) || hasStatusMoves; - } - return hasItemMoves; - } - - return false; - }) - ? 10 - : 0; - }, - 10, - ), - new WeightedModifierType( - 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"); - - if (!isHoldingOrb) { - const moveset = p - .getMoveset(true) - .filter(m => !isNullOrUndefined(m)) - .map(m => m.moveId); - const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true); - - // Moves that take advantage of obtaining the actual status effect - const hasStatusMoves = [MoveId.FACADE, MoveId.PSYCHO_SHIFT].some(m => moveset.includes(m)); - // Moves that take advantage of being able to give the target a status orb - // TODO: Take moves (Trick, Fling, Switcheroo) from comment when they are implemented - const hasItemMoves = [ - /* MoveId.TRICK, MoveId.FLING, MoveId.SWITCHEROO */ - ].some(m => moveset.includes(m)); - - if (canSetStatus) { - // Abilities that take advantage of obtaining the actual status effect, separated based on specificity to the orb - const hasGeneralAbility = [ - AbilityId.QUICK_FEET, - AbilityId.GUTS, - AbilityId.MARVEL_SCALE, - AbilityId.MAGIC_GUARD, - ].some(a => p.hasAbility(a, false, true)); - const hasSpecificAbility = [AbilityId.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); - const hasOppositeAbility = [AbilityId.TOXIC_BOOST, AbilityId.POISON_HEAL].some(a => - p.hasAbility(a, false, true), - ); - - return hasSpecificAbility || (hasGeneralAbility && !hasOppositeAbility) || hasStatusMoves; - } - return hasItemMoves; - } - - return false; - }) - ? 10 - : 0; - }, - 10, - ), - new WeightedModifierType( - 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; - } - } - - if (!isHoldingMax) { - const moveset = p.getMoveset(true).map(m => m.moveId); - - const hasAbility = [ - AbilityId.DROUGHT, - AbilityId.ORICHALCUM_PULSE, - AbilityId.DRIZZLE, - AbilityId.SAND_STREAM, - AbilityId.SAND_SPIT, - AbilityId.SNOW_WARNING, - AbilityId.ELECTRIC_SURGE, - AbilityId.HADRON_ENGINE, - AbilityId.PSYCHIC_SURGE, - AbilityId.GRASSY_SURGE, - AbilityId.SEED_SOWER, - AbilityId.MISTY_SURGE, - ].some(a => p.hasAbility(a, false, true)); - - const hasMoves = [ - MoveId.SUNNY_DAY, - MoveId.RAIN_DANCE, - MoveId.SANDSTORM, - MoveId.SNOWSCAPE, - MoveId.HAIL, - MoveId.CHILLY_RECEPTION, - MoveId.ELECTRIC_TERRAIN, - MoveId.PSYCHIC_TERRAIN, - MoveId.GRASSY_TERRAIN, - MoveId.MISTY_TERRAIN, - ].some(m => moveset.includes(m)); - - return hasAbility || hasMoves; - } - return false; - }) - ? 10 - : 0; - }, - 10, - ), - new WeightedModifierType(modifierTypes.REVIVER_SEED, 4), - new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)), - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9), - new WeightedModifierType(modifierTypes.TM_ULTRA, 11), - new WeightedModifierType(modifierTypes.RARER_CANDY, 4), - new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, skipInLastClassicWaveOrDefault(2)), - new WeightedModifierType(modifierTypes.IV_SCANNER, skipInLastClassicWaveOrDefault(4)), - new WeightedModifierType(modifierTypes.EXP_CHARM, skipInLastClassicWaveOrDefault(8)), - new WeightedModifierType(modifierTypes.EXP_SHARE, skipInLastClassicWaveOrDefault(10)), - new WeightedModifierType( - modifierTypes.TERA_ORB, - () => - !globalScene.gameMode.isClassic - ? Math.min(Math.max(Math.floor(globalScene.currentBattle.waveIndex / 50) * 2, 1), 4) - : 0, - 4, - ), - new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), - new WeightedModifierType(modifierTypes.WIDE_LENS, 7), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }); -} - -function initRogueModifierPool() { - modifierPool[ModifierTier.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), - new WeightedModifierType(modifierTypes.SHELL_BELL, 3), - new WeightedModifierType(modifierTypes.BERRY_POUCH, 4), - new WeightedModifierType(modifierTypes.GRIP_CLAW, 5), - new WeightedModifierType(modifierTypes.SCOPE_LENS, 4), - new WeightedModifierType(modifierTypes.BATON, 2), - new WeightedModifierType(modifierTypes.SOUL_DEW, 7), - new WeightedModifierType(modifierTypes.CATCHING_CHARM, () => (!globalScene.gameMode.isClassic ? 4 : 0), 4), - new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), - new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), - new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), - new WeightedModifierType(modifierTypes.LOCK_CAPSULE, () => (globalScene.gameMode.isClassic ? 0 : 3)), - new WeightedModifierType(modifierTypes.SUPER_EXP_CHARM, skipInLastClassicWaveOrDefault(8)), - new WeightedModifierType( - modifierTypes.RARE_FORM_CHANGE_ITEM, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 6, - 24, - ), - new WeightedModifierType( - modifierTypes.MEGA_BRACELET, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 9, - 36, - ), - new WeightedModifierType( - modifierTypes.DYNAMAX_BAND, - () => Math.min(Math.ceil(globalScene.currentBattle.waveIndex / 50), 4) * 9, - 36, - ), - new WeightedModifierType( - modifierTypes.VOUCHER_PLUS, - (_party: Pokemon[], rerollCount: number) => - !globalScene.gameMode.isDaily ? Math.max(3 - rerollCount * 1, 0) : 0, - 3, - ), - ].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }); -} - -/** - * Initialize the Master modifier pool - */ -function initMasterModifierPool() { - modifierPool[ModifierTier.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), - new WeightedModifierType(modifierTypes.MULTI_LENS, 18), - new WeightedModifierType( - modifierTypes.VOUCHER_PREMIUM, - (_party: Pokemon[], rerollCount: number) => - !globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly - ? Math.max(5 - rerollCount * 2, 0) - : 0, - 5, - ), - new WeightedModifierType( - modifierTypes.DNA_SPLICERS, - (party: Pokemon[]) => - !(globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) && - !globalScene.gameMode.isSplicedOnly && - party.filter(p => !p.fusionSpecies).length > 1 - ? 24 - : 0, - 24, - ), - new WeightedModifierType( - modifierTypes.MINI_BLACK_HOLE, - () => - globalScene.gameMode.isDaily || - (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE)) - ? 1 - : 0, - 1, - ), - ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }); -} - -function initTrainerModifierPool() { - trainerModifierPool[ModifierTier.COMMON] = [ - new WeightedModifierType(modifierTypes.BERRY, 8), - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }); - trainerModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }); - trainerModifierPool[ModifierTier.ULTRA] = [ - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.WHITE_HERB, 0), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }); - trainerModifierPool[ModifierTier.ROGUE] = [ - new WeightedModifierType(modifierTypes.FOCUS_BAND, 2), - new WeightedModifierType(modifierTypes.LUCKY_EGG, 4), - new WeightedModifierType(modifierTypes.QUICK_CLAW, 1), - new WeightedModifierType(modifierTypes.GRIP_CLAW, 1), - new WeightedModifierType(modifierTypes.WIDE_LENS, 1), - ].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }); - trainerModifierPool[ModifierTier.MASTER] = [ - new WeightedModifierType(modifierTypes.KINGS_ROCK, 1), - new WeightedModifierType(modifierTypes.LEFTOVERS, 1), - new WeightedModifierType(modifierTypes.SHELL_BELL, 1), - new WeightedModifierType(modifierTypes.SCOPE_LENS, 1), - ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }); -} - -/** - * Initialize the enemy buff modifier pool - */ -function initEnemyBuffModifierPool() { - enemyBuffModifierPool[ModifierTier.COMMON] = [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 9), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 9), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_POISON_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_PARALYZE_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_ATTACK_BURN_CHANCE, 3), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 9), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 4), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }); - enemyBuffModifierPool[ModifierTier.GREAT] = [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 5), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 5), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 5), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 5), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1), - ].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }); - enemyBuffModifierPool[ModifierTier.ULTRA] = [ - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 10), - new WeightedModifierType(modifierTypes.ENEMY_HEAL, 10), - new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 10), - new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 10), - new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 5), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }); - enemyBuffModifierPool[ModifierTier.ROGUE] = [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.ROGUE); - return m; - }); - enemyBuffModifierPool[ModifierTier.MASTER] = [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.MASTER); - return m; - }); -} - -/** - * Initialize the daily starter modifier pool - */ -function initDailyStarterModifierPool() { - dailyStarterModifierPool[ModifierTier.COMMON] = [ - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1), - new WeightedModifierType(modifierTypes.BERRY, 3), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }); - dailyStarterModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 5)].map( - m => { - m.setTier(ModifierTier.GREAT); - return m; - }, - ); - dailyStarterModifierPool[ModifierTier.ULTRA] = [ - new WeightedModifierType(modifierTypes.REVIVER_SEED, 4), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, 1), - new WeightedModifierType(modifierTypes.SOUL_DEW, 1), - new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, 1), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }); - dailyStarterModifierPool[ModifierTier.ROGUE] = [ - new WeightedModifierType(modifierTypes.GRIP_CLAW, 5), - new WeightedModifierType(modifierTypes.BATON, 2), - new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), - new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), - new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), - ].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }); - dailyStarterModifierPool[ModifierTier.MASTER] = [ - new WeightedModifierType(modifierTypes.LEFTOVERS, 1), - new WeightedModifierType(modifierTypes.SHELL_BELL, 1), - ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }); -} - -/** - * Initialize {@linkcode modifierPool} with the initial set of modifier types. - * {@linkcode initModifierTypes} MUST be called before this function. - */ -export function initModifierPools() { - // The modifier pools the player chooses from during modifier selection - initCommonModifierPool(); - initGreatModifierPool(); - initUltraModifierPool(); - initRogueModifierPool(); - initMasterModifierPool(); - - // Modifier pools for specific scenarios - initWildModifierPool(); - initTrainerModifierPool(); - initEnemyBuffModifierPool(); - initDailyStarterModifierPool(); -} - -/** - * High order function that returns a WeightedModifierTypeWeightFunc that will only be applied on - * classic and skip an ModifierType if current wave is greater or equal to the one passed down - * @param wave - Wave where we should stop showing the modifier - * @param defaultWeight - ModifierType default weight - * @returns A WeightedModifierTypeWeightFunc - */ -function skipInClassicAfterWave(wave: number, defaultWeight: number): WeightedModifierTypeWeightFunc { - return () => { - const gameMode = globalScene.gameMode; - const currentWave = globalScene.currentBattle.waveIndex; - return gameMode.isClassic && currentWave >= wave ? 0 : defaultWeight; - }; -} - -/** - * High order function that returns a WeightedModifierTypeWeightFunc that will only be applied on - * classic and it will skip a ModifierType if it is the last wave pull. - * @param defaultWeight ModifierType default weight - * @returns A WeightedModifierTypeWeightFunc - */ -function skipInLastClassicWaveOrDefault(defaultWeight: number): WeightedModifierTypeWeightFunc { - return skipInClassicAfterWave(199, defaultWeight); -} - -/** - * High order function that returns a WeightedModifierTypeWeightFunc to ensure Lures don't spawn on Classic 199 - * or if the lure still has over 60% of its duration left - * @param maxBattles The max battles the lure type in question lasts. 10 for green, 15 for Super, 30 for Max - * @param weight The desired weight for the lure when it does spawn - * @returns A WeightedModifierTypeWeightFunc - */ -function lureWeightFunc(maxBattles: number, weight: number): WeightedModifierTypeWeightFunc { - return () => { - const lures = globalScene.getModifiers(DoubleBattleChanceBoosterModifier); - return !(globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex === 199) && - (lures.length === 0 || - lures.filter(m => m.getMaxBattles() === maxBattles && m.getBattleCount() >= maxBattles * 0.6).length === 0) - ? weight - : 0; - }; -} - -/** - * Used to check if the player has max of a given ball type in Classic - * @param ballType The {@linkcode PokeballType} being checked - * @returns boolean: true if the player has the maximum of a given ball type - */ -function hasMaximumBalls(ballType: PokeballType): boolean { - return globalScene.gameMode.isClassic && globalScene.pokeballCounts[ballType] >= MAX_PER_TYPE_POKEBALLS; -} diff --git a/src/modifier/modifier-pools.ts b/src/modifier/modifier-pools.ts deleted file mode 100644 index d66511e3239..00000000000 --- a/src/modifier/modifier-pools.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Contains modifier pools for different contexts in the game. - * Can be safely imported without worrying about circular dependencies. - */ - -import type { ModifierPool } from "#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 deleted file mode 100644 index b359ec756e6..00000000000 --- a/src/modifier/modifier-type.ts +++ /dev/null @@ -1,2961 +0,0 @@ -import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; -import { timedEventManager } from "#app/global-event-manager"; -import { globalScene } from "#app/global-scene"; -import { getPokemonNameWithAffix } from "#app/messages"; -import Overrides from "#app/overrides"; -import { EvolutionItem, pokemonEvolutions } from "#balance/pokemon-evolutions"; -import { tmPoolTiers, tmSpecies } from "#balance/tms"; -import { getBerryEffectDescription, getBerryName } from "#data/berry"; -import { allMoves, modifierTypes } from "#data/data-lists"; -import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; -import { getNatureName, getNatureStatMultiplier } from "#data/nature"; -import { getPokeballCatchMultiplier, getPokeballName } from "#data/pokeball"; -import { pokemonFormChanges, SpeciesFormChangeCondition } from "#data/pokemon-forms"; -import { getStatusEffectDescriptor } from "#data/status-effect"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; -import { FormChangeItem } from "#enums/form-change-item"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { ModifierTier } from "#enums/modifier-tier"; -import { MoveId } from "#enums/move-id"; -import { Nature } from "#enums/nature"; -import { PokeballType } from "#enums/pokeball"; -import { PokemonType } from "#enums/pokemon-type"; -import { SpeciesFormKey } from "#enums/species-form-key"; -import { SpeciesId } from "#enums/species-id"; -import type { PermanentStat, TempBattleStat } from "#enums/stat"; -import { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; -import { StatusEffect } from "#enums/status-effect"; -import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; -import { - AddPokeballModifier, - AddVoucherModifier, - AttackTypeBoosterModifier, - BaseStatModifier, - BerryModifier, - BoostBugSpawnModifier, - BypassSpeedChanceModifier, - ContactHeldItemTransferChanceModifier, - CritBoosterModifier, - CriticalCatchChanceBoosterModifier, - DamageMoneyRewardModifier, - DoubleBattleChanceBoosterModifier, - EnemyAttackStatusEffectChanceModifier, - EnemyDamageBoosterModifier, - EnemyDamageReducerModifier, - EnemyEndureChanceModifier, - EnemyFusionChanceModifier, - type EnemyPersistentModifier, - EnemyStatusEffectHealChanceModifier, - EnemyTurnHealModifier, - EvolutionItemModifier, - EvolutionStatBoosterModifier, - EvoTrackerModifier, - ExpBalanceModifier, - ExpBoosterModifier, - ExpShareModifier, - ExtraModifierModifier, - FieldEffectModifier, - FlinchChanceModifier, - FusePokemonModifier, - GigantamaxAccessModifier, - HealingBoosterModifier, - HealShopCostModifier, - HiddenAbilityRateBoosterModifier, - HitHealModifier, - IvScannerModifier, - LevelIncrementBoosterModifier, - LockModifierTiersModifier, - MapModifier, - MegaEvolutionAccessModifier, - type Modifier, - MoneyInterestModifier, - MoneyMultiplierModifier, - MoneyRewardModifier, - MultipleParticipantExpBonusModifier, - type PersistentModifier, - 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, - TempExtraModifierModifier, - TempStatStageBoosterModifier, - TerastallizeAccessModifier, - TerrastalizeModifier, - TmModifier, - TurnHealModifier, - TurnHeldItemTransferModifier, - TurnStatusEffectModifier, -} from "#modifiers/modifier"; -import type { PokemonMove } from "#moves/pokemon-move"; -import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#system/voucher"; -import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#types/modifier-types"; -import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#ui/party-ui-handler"; -import { PartyUiHandler } from "#ui/party-ui-handler"; -import { getModifierTierTextTint } from "#ui/text"; -import { formatMoney, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#utils/common"; -import { getEnumKeys, getEnumValues } from "#utils/enums"; -import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils"; -import i18next from "i18next"; - -const outputModifierData = false; -const useMaxWeightForOutput = false; - -type NewModifierFunc = (type: ModifierType, args: any[]) => Modifier; - -export class ModifierType { - public id: string; - public localeKey: string; - public iconImage: string; - public group: string; - public soundName: string; - public tier: ModifierTier; - protected newModifierFunc: NewModifierFunc | null; - - /** - * Checks if the modifier type is of a specific type - * @param modifierType - The type to check against - * @return Whether the modifier type is of the specified type - */ - public is(modifierType: K): this is ModifierTypeInstanceMap[K] { - const targetType = ModifierTypeConstructorMap[modifierType]; - if (!targetType) { - return false; - } - return this instanceof targetType; - } - - constructor( - localeKey: string | null, - iconImage: string | null, - newModifierFunc: NewModifierFunc | null, - group?: string, - soundName?: string, - ) { - this.localeKey = localeKey!; // TODO: is this bang correct? - this.iconImage = iconImage!; // TODO: is this bang correct? - this.group = group!; // TODO: is this bang correct? - this.soundName = soundName ?? "se/restore"; - this.newModifierFunc = newModifierFunc; - } - - get name(): string { - return i18next.t(`${this.localeKey}.name` as any); - } - - getDescription(): string { - return i18next.t(`${this.localeKey}.description` as any); - } - - setTier(tier: ModifierTier): void { - this.tier = tier; - } - - 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; - } - - /** - * Populates item id for ModifierType instance - * @param func - */ - withIdFromFunc(func: ModifierTypeFunc): ModifierType { - this.id = Object.keys(modifierTypeInitObj).find(k => modifierTypeInitObj[k] === func)!; // TODO: is this bang correct? - return this; - } - - /** - * Populates item tier for ModifierType instance - * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) - * To find the tier, this function performs a reverse lookup of the item type in modifier pools - * It checks the weight of the item and will use the first tier for which the weight is greater than 0 - * This is to allow items to be in multiple item pools depending on the conditions, for example for events - * If all tiers have a weight of 0 for the item, the first tier where the item was found is used - * @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from - * @param party optional. Needed to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc}) - * if not provided or empty, the weight check will be ignored - * @param rerollCount Default `0`. Used to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc}) - */ - withTierFromPool( - poolType: ModifierPoolType = ModifierPoolType.PLAYER, - party?: PlayerPokemon[], - rerollCount = 0, - ): ModifierType { - let defaultTier: undefined | ModifierTier; - for (const tier of Object.values(getModifierPoolForType(poolType))) { - for (const modifier of tier) { - if (this.id === modifier.modifierType.id) { - let weight: number; - if (modifier.weight instanceof Function) { - weight = party ? modifier.weight(party, rerollCount) : 0; - } else { - weight = modifier.weight; - } - if (weight > 0) { - this.tier = modifier.modifierType.tier; - return this; - } - if (isNullOrUndefined(defaultTier)) { - // If weight is 0, keep track of the first tier where the item was found - defaultTier = modifier.modifierType.tier; - } - } - } - } - - // Didn't find a pool with weight > 0, fallback to first tier where the item was found, if any - if (defaultTier) { - this.tier = defaultTier; - } - - return this; - } - - newModifier(...args: any[]): Modifier | null { - // biome-ignore lint/complexity/useOptionalChain: Changing to optional would coerce null return into undefined - return this.newModifierFunc && this.newModifierFunc(this, args); - } -} - -type ModifierTypeGeneratorFunc = (party: Pokemon[], pregenArgs?: any[]) => ModifierType | null; - -export class ModifierTypeGenerator extends ModifierType { - private genTypeFunc: ModifierTypeGeneratorFunc; - - constructor(genTypeFunc: ModifierTypeGeneratorFunc) { - super(null, null, null); - this.genTypeFunc = genTypeFunc; - } - - generateType(party: Pokemon[], pregenArgs?: any[]) { - const ret = this.genTypeFunc(party, pregenArgs); - if (ret) { - ret.id = this.id; - ret.setTier(this.tier); - } - return ret; - } -} - -export interface GeneratedPersistentModifierType { - getPregenArgs(): any[]; -} - -export class AddPokeballModifierType extends ModifierType { - private pokeballType: PokeballType; - private count: number; - - constructor(iconImage: string, pokeballType: PokeballType, count: number) { - super("", iconImage, (_type, _args) => new AddPokeballModifier(this, pokeballType, count), "pb", "se/pb_bounce_1"); - this.pokeballType = pokeballType; - this.count = count; - } - - get name(): string { - return i18next.t("modifierType:ModifierType.AddPokeballModifierType.name", { - modifierCount: this.count, - pokeballName: getPokeballName(this.pokeballType), - }); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.AddPokeballModifierType.description", { - modifierCount: this.count, - pokeballName: getPokeballName(this.pokeballType), - catchRate: - getPokeballCatchMultiplier(this.pokeballType) > -1 - ? `${getPokeballCatchMultiplier(this.pokeballType)}x` - : "100%", - pokeballAmount: `${globalScene.pokeballCounts[this.pokeballType]}`, - }); - } -} - -export class AddVoucherModifierType extends ModifierType { - private voucherType: VoucherType; - private count: number; - - constructor(voucherType: VoucherType, count: number) { - super( - "", - getVoucherTypeIcon(voucherType), - (_type, _args) => new AddVoucherModifier(this, voucherType, count), - "voucher", - ); - this.count = count; - this.voucherType = voucherType; - } - - get name(): string { - return i18next.t("modifierType:ModifierType.AddVoucherModifierType.name", { - modifierCount: this.count, - voucherTypeName: getVoucherTypeName(this.voucherType), - }); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.AddVoucherModifierType.description", { - modifierCount: this.count, - voucherTypeName: getVoucherTypeName(this.voucherType), - }); - } -} - -export class PokemonModifierType extends ModifierType { - public selectFilter: PokemonSelectFilter | undefined; - - constructor( - localeKey: string, - iconImage: string, - newModifierFunc: NewModifierFunc, - selectFilter?: PokemonSelectFilter, - group?: string, - soundName?: string, - ) { - super(localeKey, iconImage, newModifierFunc, group, soundName); - - this.selectFilter = selectFilter; - } -} - -export class PokemonHeldItemModifierType extends PokemonModifierType { - constructor( - localeKey: string, - iconImage: string, - newModifierFunc: NewModifierFunc, - group?: string, - soundName?: string, - ) { - super( - localeKey, - iconImage, - newModifierFunc, - (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(); - if (!maxStackCount) { - return i18next.t("modifierType:ModifierType.PokemonHeldItemModifierType.extra.inoperable", { - pokemonName: getPokemonNameWithAffix(pokemon), - }); - } - if (matchingModifier && matchingModifier.stackCount === maxStackCount) { - return i18next.t("modifierType:ModifierType.PokemonHeldItemModifierType.extra.tooMany", { - pokemonName: getPokemonNameWithAffix(pokemon), - }); - } - return null; - }, - group, - soundName, - ); - } - - newModifier(...args: any[]): PokemonHeldItemModifier { - return super.newModifier(...args) as PokemonHeldItemModifier; - } -} - -export class TerastallizeModifierType extends PokemonModifierType { - private teraType: PokemonType; - - constructor(teraType: PokemonType) { - super( - "", - `${PokemonType[teraType].toLowerCase()}_tera_shard`, - (type, args) => new TerrastalizeModifier(type as TerastallizeModifierType, (args[0] as Pokemon).id, teraType), - (pokemon: PlayerPokemon) => { - if ( - [pokemon.species.speciesId, pokemon.fusionSpecies?.speciesId].filter( - s => s === SpeciesId.TERAPAGOS || s === SpeciesId.OGERPON || s === SpeciesId.SHEDINJA, - ).length > 0 - ) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - "tera_shard", - ); - - this.teraType = teraType; - } - - get name(): string { - return i18next.t("modifierType:ModifierType.TerastallizeModifierType.name", { - teraType: i18next.t(`pokemonInfo:Type.${PokemonType[this.teraType]}`), - }); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.TerastallizeModifierType.description", { - teraType: i18next.t(`pokemonInfo:Type.${PokemonType[this.teraType]}`), - }); - } - - getPregenArgs(): any[] { - return [this.teraType]; - } -} - -export class PokemonHpRestoreModifierType extends PokemonModifierType { - protected restorePoints: number; - protected restorePercent: number; - protected healStatus: boolean; - - constructor( - localeKey: string, - iconImage: string, - restorePoints: number, - restorePercent: number, - healStatus = false, - newModifierFunc?: NewModifierFunc, - selectFilter?: PokemonSelectFilter, - group?: string, - ) { - super( - localeKey, - iconImage, - newModifierFunc || - ((_type, args) => - new PokemonHpRestoreModifier( - this, - (args[0] as PlayerPokemon).id, - this.restorePoints, - this.restorePercent, - this.healStatus, - false, - )), - selectFilter || - ((pokemon: PlayerPokemon) => { - if ( - !pokemon.hp || - (pokemon.isFullHp() && (!this.healStatus || (!pokemon.status && !pokemon.getTag(BattlerTagType.CONFUSED)))) - ) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }), - group || "potion", - ); - - this.restorePoints = restorePoints; - this.restorePercent = restorePercent; - this.healStatus = healStatus; - } - - getDescription(): string { - return this.restorePoints - ? i18next.t("modifierType:ModifierType.PokemonHpRestoreModifierType.description", { - restorePoints: this.restorePoints, - restorePercent: this.restorePercent, - }) - : this.healStatus - ? i18next.t("modifierType:ModifierType.PokemonHpRestoreModifierType.extra.fullyWithStatus") - : i18next.t("modifierType:ModifierType.PokemonHpRestoreModifierType.extra.fully"); - } -} - -export class PokemonReviveModifierType extends PokemonHpRestoreModifierType { - constructor(localeKey: string, iconImage: string, restorePercent: number) { - super( - localeKey, - iconImage, - 0, - restorePercent, - false, - (_type, args) => - new PokemonHpRestoreModifier(this, (args[0] as PlayerPokemon).id, 0, this.restorePercent, false, true), - (pokemon: PlayerPokemon) => { - if (!pokemon.isFainted()) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - "revive", - ); - - this.selectFilter = (pokemon: PlayerPokemon) => { - if (pokemon.hp) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonReviveModifierType.description", { - restorePercent: this.restorePercent, - }); - } -} - -export class PokemonStatusHealModifierType extends PokemonModifierType { - constructor(localeKey: string, iconImage: string) { - super( - localeKey, - iconImage, - (_type, args) => new PokemonStatusHealModifier(this, (args[0] as PlayerPokemon).id), - (pokemon: PlayerPokemon) => { - if (!pokemon.hp || (!pokemon.status && !pokemon.getTag(BattlerTagType.CONFUSED))) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - ); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonStatusHealModifierType.description"); - } -} - -export abstract class PokemonMoveModifierType extends PokemonModifierType { - public moveSelectFilter: PokemonMoveSelectFilter | undefined; - - constructor( - localeKey: string, - iconImage: string, - newModifierFunc: NewModifierFunc, - selectFilter?: PokemonSelectFilter, - moveSelectFilter?: PokemonMoveSelectFilter, - group?: string, - ) { - super(localeKey, iconImage, newModifierFunc, selectFilter, group); - - this.moveSelectFilter = moveSelectFilter; - } -} - -export class PokemonPpRestoreModifierType extends PokemonMoveModifierType { - protected restorePoints: number; - - constructor(localeKey: string, iconImage: string, restorePoints: number) { - super( - localeKey, - iconImage, - (_type, args) => - new PokemonPpRestoreModifier(this, (args[0] as PlayerPokemon).id, args[1] as number, this.restorePoints), - (_pokemon: PlayerPokemon) => { - return null; - }, - (pokemonMove: PokemonMove) => { - if (!pokemonMove.ppUsed) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - "ether", - ); - - this.restorePoints = restorePoints; - } - - getDescription(): string { - return this.restorePoints > -1 - ? i18next.t("modifierType:ModifierType.PokemonPpRestoreModifierType.description", { - restorePoints: this.restorePoints, - }) - : i18next.t("modifierType:ModifierType.PokemonPpRestoreModifierType.extra.fully"); - } -} - -export class PokemonAllMovePpRestoreModifierType extends PokemonModifierType { - protected restorePoints: number; - - constructor(localeKey: string, iconImage: string, restorePoints: number) { - super( - localeKey, - iconImage, - (_type, args) => new PokemonAllMovePpRestoreModifier(this, (args[0] as PlayerPokemon).id, this.restorePoints), - (pokemon: PlayerPokemon) => { - if (!pokemon.getMoveset().filter(m => m.ppUsed).length) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - "elixir", - ); - - this.restorePoints = restorePoints; - } - - getDescription(): string { - return this.restorePoints > -1 - ? i18next.t("modifierType:ModifierType.PokemonAllMovePpRestoreModifierType.description", { - restorePoints: this.restorePoints, - }) - : i18next.t("modifierType:ModifierType.PokemonAllMovePpRestoreModifierType.extra.fully"); - } -} - -export class PokemonPpUpModifierType extends PokemonMoveModifierType { - protected upPoints: number; - - constructor(localeKey: string, iconImage: string, upPoints: number) { - super( - localeKey, - iconImage, - (_type, args) => new PokemonPpUpModifier(this, (args[0] as PlayerPokemon).id, args[1] as number, this.upPoints), - (_pokemon: PlayerPokemon) => { - return null; - }, - (pokemonMove: PokemonMove) => { - if (pokemonMove.getMove().pp < 5 || pokemonMove.ppUp >= 3 || pokemonMove.maxPpOverride) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - "ppUp", - ); - - this.upPoints = upPoints; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonPpUpModifierType.description", { upPoints: this.upPoints }); - } -} - -export class PokemonNatureChangeModifierType extends PokemonModifierType { - protected nature: Nature; - - constructor(nature: Nature) { - super( - "", - `mint_${ - getEnumKeys(Stat) - .find(s => getNatureStatMultiplier(nature, Stat[s]) > 1) - ?.toLowerCase() || "neutral" - }`, - (_type, args) => new PokemonNatureChangeModifier(this, (args[0] as PlayerPokemon).id, this.nature), - (pokemon: PlayerPokemon) => { - if (pokemon.getNature() === this.nature) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - "mint", - ); - - this.nature = nature; - } - - get name(): string { - return i18next.t("modifierType:ModifierType.PokemonNatureChangeModifierType.name", { - natureName: getNatureName(this.nature), - }); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonNatureChangeModifierType.description", { - natureName: getNatureName(this.nature, true, true, true), - }); - } -} - -export class RememberMoveModifierType extends PokemonModifierType { - constructor(localeKey: string, iconImage: string, group?: string) { - super( - localeKey, - iconImage, - (type, args) => new RememberMoveModifier(type, (args[0] as PlayerPokemon).id, args[1] as number), - (pokemon: PlayerPokemon) => { - if (!pokemon.getLearnableLevelMoves().length) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - group, - ); - } -} - -export class DoubleBattleChanceBoosterModifierType extends ModifierType { - private maxBattles: number; - - constructor(localeKey: string, iconImage: string, maxBattles: number) { - super(localeKey, iconImage, (_type, _args) => new DoubleBattleChanceBoosterModifier(this, maxBattles), "lure"); - - this.maxBattles = maxBattles; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", { - battleCount: this.maxBattles, - }); - } -} - -export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType { - private stat: TempBattleStat; - private nameKey: string; - private quantityKey: string; - - constructor(stat: TempBattleStat) { - const nameKey = TempStatStageBoosterModifierTypeGenerator.items[stat]; - super("", nameKey, (_type, _args) => new TempStatStageBoosterModifier(this, this.stat, 5)); - - this.stat = stat; - this.nameKey = nameKey; - this.quantityKey = stat !== Stat.ACC ? "percentage" : "stage"; - } - - get name(): string { - return i18next.t(`modifierType:TempStatStageBoosterItem.${this.nameKey}`); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { - stat: i18next.t(getStatKey(this.stat)), - amount: i18next.t(`modifierType:ModifierType.TempStatStageBoosterModifierType.extra.${this.quantityKey}`), - }); - } - - getPregenArgs(): any[] { - return [this.stat]; - } -} - -export class BerryModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { - private berryType: BerryType; - - constructor(berryType: BerryType) { - super( - "", - `${BerryType[berryType].toLowerCase()}_berry`, - (type, args) => new BerryModifier(type, (args[0] as Pokemon).id, berryType), - "berry", - ); - - this.berryType = berryType; - this.id = "BERRY"; // needed to prevent harvest item deletion; remove after modifier rework - } - - get name(): string { - return getBerryName(this.berryType); - } - - getDescription(): string { - return getBerryEffectDescription(this.berryType); - } - - getPregenArgs(): any[] { - return [this.berryType]; - } -} - -enum AttackTypeBoosterItem { - SILK_SCARF, - BLACK_BELT, - SHARP_BEAK, - POISON_BARB, - SOFT_SAND, - HARD_STONE, - SILVER_POWDER, - SPELL_TAG, - METAL_COAT, - CHARCOAL, - MYSTIC_WATER, - MIRACLE_SEED, - MAGNET, - TWISTED_SPOON, - NEVER_MELT_ICE, - DRAGON_FANG, - BLACK_GLASSES, - FAIRY_FEATHER, -} - -export class AttackTypeBoosterModifierType - extends PokemonHeldItemModifierType - implements GeneratedPersistentModifierType -{ - 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), - ); - - 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( - localeKey, - iconImage, - (_type, args) => new PokemonLevelIncrementModifier(this, (args[0] as PlayerPokemon).id), - (_pokemon: PlayerPokemon) => null, - ); - } - - getDescription(): string { - let levels = 1; - const hasCandyJar = globalScene.modifiers.find(modifier => modifier instanceof LevelIncrementBoosterModifier); - if (hasCandyJar) { - levels += hasCandyJar.stackCount; - } - return i18next.t("modifierType:ModifierType.PokemonLevelIncrementModifierType.description", { levels }); - } -} - -export class AllPokemonLevelIncrementModifierType extends ModifierType { - constructor(localeKey: string, iconImage: string) { - super(localeKey, iconImage, (_type, _args) => new PokemonLevelIncrementModifier(this, -1)); - } - - getDescription(): string { - let levels = 1; - const hasCandyJar = globalScene.modifiers.find(modifier => modifier instanceof LevelIncrementBoosterModifier); - if (hasCandyJar) { - levels += hasCandyJar.stackCount; - } - return i18next.t("modifierType:ModifierType.AllPokemonLevelIncrementModifierType.description", { levels }); - } -} - -export class BaseStatBoosterModifierType - extends PokemonHeldItemModifierType - implements GeneratedPersistentModifierType -{ - 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)); - - this.stat = stat; - this.key = key; - } - - get name(): string { - return i18next.t(`modifierType:BaseStatBoosterItem.${this.key}`); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", { - stat: i18next.t(getStatKey(this.stat)), - }); - } - - getPregenArgs(): any[] { - return [this.stat]; - } -} - -/** - * Shuckle Juice item - */ -export class PokemonBaseStatTotalModifierType - extends PokemonHeldItemModifierType - implements GeneratedPersistentModifierType -{ - private readonly statModifier: 10 | -15; - - constructor(statModifier: 10 | -15) { - super( - statModifier > 0 - ? "modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD" - : "modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD", - statModifier > 0 ? "berry_juice_good" : "berry_juice_bad", - (_type, args) => new PokemonBaseStatTotalModifier(this, (args[0] as Pokemon).id, statModifier), - ); - this.statModifier = statModifier; - } - - override getDescription(): string { - return this.statModifier > 0 - ? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.description") - : i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.description"); - } - - public getPregenArgs(): any[] { - return [this.statModifier]; - } -} - -class AllPokemonFullHpRestoreModifierType extends ModifierType { - private descriptionKey: string; - - constructor(localeKey: string, iconImage: string, descriptionKey?: string, newModifierFunc?: NewModifierFunc) { - super( - localeKey, - iconImage, - newModifierFunc || ((_type, _args) => new PokemonHpRestoreModifier(this, -1, 0, 100, false)), - ); - - this.descriptionKey = descriptionKey!; // TODO: is this bang correct? - } - - getDescription(): string { - return i18next.t( - `${this.descriptionKey || "modifierType:ModifierType.AllPokemonFullHpRestoreModifierType"}.description` as any, - ); - } -} - -class AllPokemonFullReviveModifierType extends AllPokemonFullHpRestoreModifierType { - constructor(localeKey: string, iconImage: string) { - super( - localeKey, - iconImage, - "modifierType:ModifierType.AllPokemonFullReviveModifierType", - (_type, _args) => new PokemonHpRestoreModifier(this, -1, 0, 100, false, true), - ); - } -} - -export class MoneyRewardModifierType extends ModifierType { - private moneyMultiplier: number; - private moneyMultiplierDescriptorKey: string; - - constructor(localeKey: string, iconImage: string, moneyMultiplier: number, moneyMultiplierDescriptorKey: string) { - super(localeKey, iconImage, (_type, _args) => new MoneyRewardModifier(this, moneyMultiplier), "money", "se/buy"); - - this.moneyMultiplier = moneyMultiplier; - this.moneyMultiplierDescriptorKey = moneyMultiplierDescriptorKey; - } - - getDescription(): string { - const moneyAmount = new NumberHolder(globalScene.getWaveMoneyAmount(this.moneyMultiplier)); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); - const formattedMoney = formatMoney(globalScene.moneyFormat, moneyAmount.value); - - return i18next.t("modifierType:ModifierType.MoneyRewardModifierType.description", { - moneyMultiplier: i18next.t(this.moneyMultiplierDescriptorKey as any), - moneyAmount: formattedMoney, - }); - } -} - -export class ExpBoosterModifierType extends ModifierType { - private boostPercent: number; - - constructor(localeKey: string, iconImage: string, boostPercent: number) { - super(localeKey, iconImage, () => new ExpBoosterModifier(this, boostPercent)); - - this.boostPercent = boostPercent; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.ExpBoosterModifierType.description", { - boostPercent: this.boostPercent, - }); - } -} - -export class PokemonExpBoosterModifierType extends PokemonHeldItemModifierType { - private boostPercent: number; - - constructor(localeKey: string, iconImage: string, boostPercent: number) { - super( - localeKey, - iconImage, - (_type, args) => new PokemonExpBoosterModifier(this, (args[0] as Pokemon).id, boostPercent), - ); - - this.boostPercent = boostPercent; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonExpBoosterModifierType.description", { - boostPercent: this.boostPercent, - }); - } -} - -export class PokemonFriendshipBoosterModifierType extends PokemonHeldItemModifierType { - constructor(localeKey: string, iconImage: string) { - super(localeKey, iconImage, (_type, args) => new PokemonFriendshipBoosterModifier(this, (args[0] as Pokemon).id)); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonFriendshipBoosterModifierType.description"); - } -} - -export class PokemonMoveAccuracyBoosterModifierType extends PokemonHeldItemModifierType { - private amount: number; - - constructor(localeKey: string, iconImage: string, amount: number, group?: string, soundName?: string) { - super( - localeKey, - iconImage, - (_type, args) => new PokemonMoveAccuracyBoosterModifier(this, (args[0] as Pokemon).id, amount), - group, - soundName, - ); - - this.amount = amount; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonMoveAccuracyBoosterModifierType.description", { - accuracyAmount: this.amount, - }); - } -} - -export class PokemonMultiHitModifierType extends PokemonHeldItemModifierType { - constructor(localeKey: string, iconImage: string) { - super( - localeKey, - iconImage, - (type, args) => new PokemonMultiHitModifier(type as PokemonMultiHitModifierType, (args[0] as Pokemon).id), - ); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.PokemonMultiHitModifierType.description"); - } -} - -export class TmModifierType extends PokemonModifierType { - public moveId: MoveId; - - constructor(moveId: MoveId) { - super( - "", - `tm_${PokemonType[allMoves[moveId].type].toLowerCase()}`, - (_type, args) => new TmModifier(this, (args[0] as PlayerPokemon).id), - (pokemon: PlayerPokemon) => { - if ( - pokemon.compatibleTms.indexOf(moveId) === -1 || - pokemon.getMoveset().filter(m => m.moveId === moveId).length - ) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - "tm", - ); - - this.moveId = moveId; - } - - get name(): string { - return i18next.t("modifierType:ModifierType.TmModifierType.name", { - moveId: padInt(Object.keys(tmSpecies).indexOf(this.moveId.toString()) + 1, 3), - moveName: allMoves[this.moveId].name, - }); - } - - getDescription(): string { - return i18next.t( - globalScene.enableMoveInfo - ? "modifierType:ModifierType.TmModifierTypeWithInfo.description" - : "modifierType:ModifierType.TmModifierType.description", - { moveName: allMoves[this.moveId].name }, - ); - } -} - -export class EvolutionItemModifierType extends PokemonModifierType implements GeneratedPersistentModifierType { - public evolutionItem: EvolutionItem; - - constructor(evolutionItem: EvolutionItem) { - super( - "", - EvolutionItem[evolutionItem].toLowerCase(), - (_type, args) => new EvolutionItemModifier(this, (args[0] as PlayerPokemon).id), - (pokemon: PlayerPokemon) => { - if ( - pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && - pokemonEvolutions[pokemon.species.speciesId].filter(e => e.validate(pokemon, false, this.evolutionItem)) - .length && - pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX - ) { - return null; - } - if ( - pokemon.isFusion() && - pokemon.fusionSpecies && - pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) && - pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(e => e.validate(pokemon, true, this.evolutionItem)) - .length && - pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX - ) { - return null; - } - - return PartyUiHandler.NoEffectMessage; - }, - ); - - this.evolutionItem = evolutionItem; - } - - get name(): string { - return i18next.t(`modifierType:EvolutionItem.${EvolutionItem[this.evolutionItem]}`); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.EvolutionItemModifierType.description"); - } - - getPregenArgs(): any[] { - return [this.evolutionItem]; - } -} - -/** - * Class that represents form changing items - */ -export class FormChangeItemModifierType extends PokemonModifierType implements GeneratedPersistentModifierType { - public formChangeItem: FormChangeItem; - - constructor(formChangeItem: FormChangeItem) { - super( - "", - FormChangeItem[formChangeItem].toLowerCase(), - (_type, args) => new PokemonFormChangeItemModifier(this, (args[0] as PlayerPokemon).id, formChangeItem, true), - (pokemon: PlayerPokemon) => { - // Make sure the Pokemon has alternate forms - if ( - pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId) && - // Get all form changes for this species with an item trigger, including any compound triggers - pokemonFormChanges[pokemon.species.speciesId] - .filter( - fc => fc.trigger.hasTriggerType(SpeciesFormChangeItemTrigger) && fc.preFormKey === pokemon.getFormKey(), - ) - // Returns true if any form changes match this item - .flatMap(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) - .flatMap(fc => fc.item) - .includes(this.formChangeItem) - ) { - return null; - } - - return PartyUiHandler.NoEffectMessage; - }, - ); - - this.formChangeItem = formChangeItem; - } - - get name(): string { - return i18next.t(`modifierType:FormChangeItem.${FormChangeItem[this.formChangeItem]}`); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.FormChangeItemModifierType.description"); - } - - getPregenArgs(): any[] { - return [this.formChangeItem]; - } -} - -export class FusePokemonModifierType extends PokemonModifierType { - constructor(localeKey: string, iconImage: string) { - super( - localeKey, - iconImage, - (_type, args) => new FusePokemonModifier(this, (args[0] as PlayerPokemon).id, (args[1] as PlayerPokemon).id), - (pokemon: PlayerPokemon) => { - if (pokemon.isFusion()) { - return PartyUiHandler.NoEffectMessage; - } - return null; - }, - ); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.FusePokemonModifierType.description"); - } -} - -class AttackTypeBoosterModifierTypeGenerator 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); - } - - 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 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); - }); - } -} - -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", - }; - - constructor() { - super((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs) { - return new BaseStatBoosterModifierType(pregenArgs[0]); - } - const randStat: PermanentStat = randSeedInt(Stat.SPD + 1); - return new BaseStatBoosterModifierType(randStat); - }); - } -} - -class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator { - public static readonly items: Record = { - [Stat.ATK]: "x_attack", - [Stat.DEF]: "x_defense", - [Stat.SPATK]: "x_sp_atk", - [Stat.SPDEF]: "x_sp_def", - [Stat.SPD]: "x_speed", - [Stat.ACC]: "x_accuracy", - }; - - constructor() { - super((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs && pregenArgs.length === 1 && TEMP_BATTLE_STATS.includes(pregenArgs[0])) { - return new TempStatStageBoosterModifierType(pregenArgs[0]); - } - const randStat: TempBattleStat = randSeedInt(Stat.ACC, Stat.ATK); - return new TempStatStageBoosterModifierType(randStat); - }); - } -} - -/** - * Modifier type generator for {@linkcode SpeciesStatBoosterModifierType}, which - * encapsulates the logic for weighting the most useful held item from - * the current list of {@linkcode items}. - * @extends ModifierTypeGenerator - */ -class SpeciesStatBoosterModifierTypeGenerator 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); - } - - // 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); - } - - for (const p of party) { - const speciesId = p.getSpeciesForm(true).speciesId; - const fusionSpeciesId = p.isFusion() ? p.getFusionSpeciesForm(true).speciesId : null; - // 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; - - // 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]), - ); - - if (!hasItem) { - if (checkedSpecies.includes(speciesId) || (!!fusionSpeciesId && checkedSpecies.includes(fusionSpeciesId))) { - // Add weight if party member has a matching species or, if applicable, a matching fusion species - weights[i]++; - } else if (checkedSpecies.includes(SpeciesId.PIKACHU) && hasFling) { - // Add weight to Light Ball if party member has Fling - weights[i]++; - } - } - } - } - - let totalWeight = 0; - for (const weight of weights) { - totalWeight += weight; - } - - if (totalWeight !== 0) { - const randInt = randSeedInt(totalWeight, 1); - let weight = 0; - - for (const i in weights) { - if (weights[i] !== 0) { - const curWeight = weight + weights[i]; - if (randInt <= weight + weights[i]) { - return new SpeciesStatBoosterModifierType(keys[i] as SpeciesStatBoosterItem); - } - weight = curWeight; - } - } - } - - return null; - }); - } -} - -class TmModifierTypeGenerator extends ModifierTypeGenerator { - constructor(tier: ModifierTier) { - super((party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in MoveId) { - return new TmModifierType(pregenArgs[0] as MoveId); - } - const partyMemberCompatibleTms = party.map(p => { - const previousLevelMoves = p.getLearnableLevelMoves(); - return (p as PlayerPokemon).compatibleTms.filter( - tm => !p.moveset.find(m => m.moveId === tm) && !previousLevelMoves.find(lm => lm === tm), - ); - }); - const tierUniqueCompatibleTms = partyMemberCompatibleTms - .flat() - .filter(tm => tmPoolTiers[tm] === tier) - .filter(tm => !allMoves[tm].name.endsWith(" (N)")) - .filter((tm, i, array) => array.indexOf(tm) === i); - if (!tierUniqueCompatibleTms.length) { - return null; - } - // TODO: should this use `randSeedItem`? - const randTmIndex = randSeedInt(tierUniqueCompatibleTms.length); - return new TmModifierType(tierUniqueCompatibleTms[randTmIndex]); - }); - } -} - -class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { - constructor(rare: boolean) { - super((party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in EvolutionItem) { - return new EvolutionItemModifierType(pregenArgs[0] as EvolutionItem); - } - - const evolutionItemPool = [ - party - .filter( - p => - pokemonEvolutions.hasOwnProperty(p.species.speciesId) && - (!p.pauseEvolutions || - p.species.speciesId === SpeciesId.SLOWPOKE || - p.species.speciesId === SpeciesId.EEVEE || - p.species.speciesId === SpeciesId.KIRLIA || - p.species.speciesId === SpeciesId.SNORUNT), - ) - .flatMap(p => { - const evolutions = pokemonEvolutions[p.species.speciesId]; - return evolutions.filter(e => e.isValidItemEvolution(p)); - }), - party - .filter( - p => - p.isFusion() && - p.fusionSpecies && - pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId) && - (!p.pauseEvolutions || - p.fusionSpecies.speciesId === SpeciesId.SLOWPOKE || - p.fusionSpecies.speciesId === SpeciesId.EEVEE || - p.fusionSpecies.speciesId === SpeciesId.KIRLIA || - p.fusionSpecies.speciesId === SpeciesId.SNORUNT), - ) - .flatMap(p => { - const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId]; - return evolutions.filter(e => e.isValidItemEvolution(p, true)); - }), - ] - .flat() - .flatMap(e => e.evoItem) - .filter(i => !!i && i > 50 === rare); - - if (!evolutionItemPool.length) { - return null; - } - - // TODO: should this use `randSeedItem`? - return new EvolutionItemModifierType(evolutionItemPool[randSeedInt(evolutionItemPool.length)]!); // TODO: is the bang correct? - }); - } -} - -export class FormChangeItemModifierTypeGenerator 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); - } - - const formChangeItemPool = [ - ...new Set( - party - .filter(p => pokemonFormChanges.hasOwnProperty(p.species.speciesId)) - .flatMap(p => { - const formChanges = pokemonFormChanges[p.species.speciesId]; - let formChangeItemTriggers = formChanges - .filter( - fc => - ((fc.formKey.indexOf(SpeciesFormKey.MEGA) === -1 && - fc.formKey.indexOf(SpeciesFormKey.PRIMAL) === -1) || - globalScene.getModifiers(MegaEvolutionAccessModifier).length) && - ((fc.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) === -1 && - fc.formKey.indexOf(SpeciesFormKey.ETERNAMAX) === -1) || - globalScene.getModifiers(GigantamaxAccessModifier).length) && - (!fc.conditions.length || - fc.conditions.filter(cond => cond instanceof SpeciesFormChangeCondition && cond.predicate(p)) - .length) && - fc.preFormKey === p.getFormKey(), - ) - .map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) - .filter( - t => - t?.active && - !globalScene.findModifier( - m => - m instanceof PokemonFormChangeItemModifier && - m.pokemonId === p.id && - m.formChangeItem === t.item, - ), - ); - - 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... - let foundULTRA_Z = false, - foundN_LUNA = false, - foundN_SOLAR = false; - formChangeItemTriggers.forEach((fc, _i) => { - console.log("Checking ", fc.item); - switch (fc.item) { - case FormChangeItem.ULTRANECROZIUM_Z: - foundULTRA_Z = true; - break; - case FormChangeItem.N_LUNARIZER: - foundN_LUNA = true; - break; - case FormChangeItem.N_SOLARIZER: - foundN_SOLAR = true; - break; - } - }); - if (foundULTRA_Z && foundN_LUNA && foundN_SOLAR) { - // all three items are present -> user hasn't acquired any of the N_*ARIZERs -> block ULTRANECROZIUM_Z acquisition. - formChangeItemTriggers = formChangeItemTriggers.filter( - fc => fc.item !== FormChangeItem.ULTRANECROZIUM_Z, - ); - } else { - console.log("DID NOT FIND "); - } - } - return formChangeItemTriggers; - }), - ), - ] - .flat() - .flatMap(fc => fc.item) - .filter(i => (i && i < 100) === isRareFormChangeItem); - // convert it into a set to remove duplicate values, which can appear when the same species with a potential form change is in the party. - - if (!formChangeItemPool.length) { - return null; - } - - // TODO: should this use `randSeedItem`? - return new FormChangeItemModifierType(formChangeItemPool[randSeedInt(formChangeItemPool.length)]); - }); - } -} - -export class ContactHeldItemTransferChanceModifierType extends PokemonHeldItemModifierType { - private chancePercent: number; - - constructor(localeKey: string, iconImage: string, chancePercent: number, group?: string, soundName?: string) { - super( - localeKey, - iconImage, - (type, args) => new ContactHeldItemTransferChanceModifier(type, (args[0] as Pokemon).id, chancePercent), - group, - soundName, - ); - - this.chancePercent = chancePercent; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.ContactHeldItemTransferChanceModifierType.description", { - chancePercent: this.chancePercent, - }); - } -} - -export class TurnHeldItemTransferModifierType extends PokemonHeldItemModifierType { - constructor(localeKey: string, iconImage: string, group?: string, soundName?: string) { - super( - localeKey, - iconImage, - (type, args) => new TurnHeldItemTransferModifier(type, (args[0] as Pokemon).id), - group, - soundName, - ); - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.TurnHeldItemTransferModifierType.description"); - } -} - -export class EnemyAttackStatusEffectChanceModifierType extends ModifierType { - private chancePercent: number; - private effect: StatusEffect; - - constructor(localeKey: string, iconImage: string, chancePercent: number, effect: StatusEffect, stackCount?: number) { - super( - localeKey, - iconImage, - (type, _args) => new EnemyAttackStatusEffectChanceModifier(type, effect, chancePercent, stackCount), - "enemy_status_chance", - ); - - this.chancePercent = chancePercent; - this.effect = effect; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.EnemyAttackStatusEffectChanceModifierType.description", { - chancePercent: this.chancePercent, - statusEffect: getStatusEffectDescriptor(this.effect), - }); - } -} - -export class EnemyEndureChanceModifierType extends ModifierType { - private chancePercent: number; - - constructor(localeKey: string, iconImage: string, chancePercent: number) { - super(localeKey, iconImage, (type, _args) => new EnemyEndureChanceModifier(type, chancePercent), "enemy_endure"); - - this.chancePercent = chancePercent; - } - - getDescription(): string { - return i18next.t("modifierType:ModifierType.EnemyEndureChanceModifierType.description", { - chancePercent: this.chancePercent, - }); - } -} - -export class WeightedModifierType { - public modifierType: ModifierType; - public weight: number | WeightedModifierTypeWeightFunc; - public maxWeight: number | WeightedModifierTypeWeightFunc; - - constructor( - modifierTypeFunc: ModifierTypeFunc, - weight: number | WeightedModifierTypeWeightFunc, - maxWeight?: number | WeightedModifierTypeWeightFunc, - ) { - this.modifierType = modifierTypeFunc(); - this.modifierType.id = Object.keys(modifierTypeInitObj).find(k => modifierTypeInitObj[k] === modifierTypeFunc)!; // TODO: is this bang correct? - this.weight = weight; - this.maxWeight = maxWeight || (!(weight instanceof Function) ? weight : 0); - } - - setTier(tier: ModifierTier) { - this.modifierType.setTier(tier); - } -} - -type BaseModifierOverride = { - name: Exclude; - count?: number; -}; - -/** Type for modifiers and held items that are constructed via {@linkcode ModifierTypeGenerator}. */ -export type GeneratorModifierOverride = { - count?: number; -} & ( - | { - name: keyof Pick; - type?: SpeciesStatBoosterItem; - } - | { - name: keyof Pick; - type?: TempBattleStat; - } - | { - name: keyof Pick; - type?: Stat; - } - | { - name: keyof Pick; - type?: Nature; - } - | { - name: keyof Pick; - type?: PokemonType; - } - | { - name: keyof Pick; - type?: BerryType; - } - | { - name: keyof Pick; - type?: EvolutionItem; - } - | { - name: keyof Pick; - type?: FormChangeItem; - } - | { - name: keyof Pick; - type?: MoveId; - } -); - -/** Type used to construct modifiers and held items for overriding purposes. */ -export type ModifierOverride = GeneratorModifierOverride | BaseModifierOverride; - -export type ModifierTypeKeys = keyof typeof modifierTypeInitObj; - -const modifierTypeInitObj = Object.freeze({ - POKEBALL: () => new AddPokeballModifierType("pb", PokeballType.POKEBALL, 5), - GREAT_BALL: () => new AddPokeballModifierType("gb", PokeballType.GREAT_BALL, 5), - ULTRA_BALL: () => new AddPokeballModifierType("ub", PokeballType.ULTRA_BALL, 5), - ROGUE_BALL: () => new AddPokeballModifierType("rb", PokeballType.ROGUE_BALL, 5), - MASTER_BALL: () => new AddPokeballModifierType("mb", PokeballType.MASTER_BALL, 1), - - RARE_CANDY: () => new PokemonLevelIncrementModifierType("modifierType:ModifierType.RARE_CANDY", "rare_candy"), - RARER_CANDY: () => new AllPokemonLevelIncrementModifierType("modifierType:ModifierType.RARER_CANDY", "rarer_candy"), - - 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), - ), - - MEGA_BRACELET: () => - new ModifierType( - "modifierType:ModifierType.MEGA_BRACELET", - "mega_bracelet", - (type, _args) => new MegaEvolutionAccessModifier(type), - ), - DYNAMAX_BAND: () => - new ModifierType( - "modifierType:ModifierType.DYNAMAX_BAND", - "dynamax_band", - (type, _args) => new GigantamaxAccessModifier(type), - ), - TERA_ORB: () => - new ModifierType( - "modifierType:ModifierType.TERA_ORB", - "tera_orb", - (type, _args) => new TerastallizeAccessModifier(type), - ), - - MAP: () => new ModifierType("modifierType:ModifierType.MAP", "map", (type, _args) => new MapModifier(type)), - - POTION: () => new PokemonHpRestoreModifierType("modifierType:ModifierType.POTION", "potion", 20, 10), - SUPER_POTION: () => - new PokemonHpRestoreModifierType("modifierType:ModifierType.SUPER_POTION", "super_potion", 50, 25), - HYPER_POTION: () => - new PokemonHpRestoreModifierType("modifierType:ModifierType.HYPER_POTION", "hyper_potion", 200, 50), - MAX_POTION: () => new PokemonHpRestoreModifierType("modifierType:ModifierType.MAX_POTION", "max_potion", 0, 100), - FULL_RESTORE: () => - new PokemonHpRestoreModifierType("modifierType:ModifierType.FULL_RESTORE", "full_restore", 0, 100, true), - - REVIVE: () => new PokemonReviveModifierType("modifierType:ModifierType.REVIVE", "revive", 50), - MAX_REVIVE: () => new PokemonReviveModifierType("modifierType:ModifierType.MAX_REVIVE", "max_revive", 100), - - FULL_HEAL: () => new PokemonStatusHealModifierType("modifierType:ModifierType.FULL_HEAL", "full_heal"), - - 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), - ), - - ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.ETHER", "ether", 10), - MAX_ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.MAX_ETHER", "max_ether", -1), - - ELIXIR: () => new PokemonAllMovePpRestoreModifierType("modifierType:ModifierType.ELIXIR", "elixir", 10), - MAX_ELIXIR: () => new PokemonAllMovePpRestoreModifierType("modifierType:ModifierType.MAX_ELIXIR", "max_elixir", -1), - - PP_UP: () => new PokemonPpUpModifierType("modifierType:ModifierType.PP_UP", "pp_up", 1), - PP_MAX: () => new PokemonPpUpModifierType("modifierType:ModifierType.PP_MAX", "pp_max", 3), - - /*REPEL: () => new DoubleBattleChanceBoosterModifierType('Repel', 5), - SUPER_REPEL: () => new DoubleBattleChanceBoosterModifierType('Super Repel', 10), - MAX_REPEL: () => new DoubleBattleChanceBoosterModifierType('Max Repel', 25),*/ - - LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.LURE", "lure", 10), - SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 15), - MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 30), - - SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(false), - RARE_SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(true), - - TEMP_STAT_STAGE_BOOSTER: () => new TempStatStageBoosterModifierTypeGenerator(), - - DIRE_HIT: () => - new (class extends ModifierType { - getDescription(): string { - return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { - stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises"), - amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.stage"), - }); - } - })("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new TempCritBoosterModifier(type, 5)), - - BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(), - - ATTACK_TYPE_BOOSTER: () => new AttackTypeBoosterModifierTypeGenerator(), - - MINT: () => - new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in Nature) { - return new PokemonNatureChangeModifierType(pregenArgs[0] as Nature); - } - return new PokemonNatureChangeModifierType(randSeedItem(getEnumValues(Nature))); - }), - - MYSTICAL_ROCK: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.MYSTICAL_ROCK", - "mystical_rock", - (type, args) => new FieldEffectModifier(type, (args[0] as Pokemon).id), - ), - - TERA_SHARD: () => - new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) { - return new TerastallizeModifierType(pregenArgs[0] as PokemonType); - } - if (!globalScene.getModifiers(TerastallizeAccessModifier).length) { - return null; - } - const teraTypes: PokemonType[] = []; - for (const p of party) { - if ( - !(p.hasSpecies(SpeciesId.TERAPAGOS) || p.hasSpecies(SpeciesId.OGERPON) || p.hasSpecies(SpeciesId.SHEDINJA)) - ) { - teraTypes.push(p.teraType); - } - } - let excludedType = PokemonType.UNKNOWN; - if (teraTypes.length > 0 && teraTypes.filter(t => t === teraTypes[0]).length === teraTypes.length) { - excludedType = teraTypes[0]; - } - let shardType = randSeedInt(64) ? (randSeedInt(18) as PokemonType) : PokemonType.STELLAR; - while (shardType === excludedType) { - shardType = randSeedInt(64) ? (randSeedInt(18) as PokemonType) : PokemonType.STELLAR; - } - 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); - }), - - TM_COMMON: () => new TmModifierTypeGenerator(ModifierTier.COMMON), - TM_GREAT: () => new TmModifierTypeGenerator(ModifierTier.GREAT), - TM_ULTRA: () => new TmModifierTypeGenerator(ModifierTier.ULTRA), - - MEMORY_MUSHROOM: () => new RememberMoveModifierType("modifierType:ModifierType.MEMORY_MUSHROOM", "big_mushroom"), - - EXP_SHARE: () => - new ModifierType("modifierType:ModifierType.EXP_SHARE", "exp_share", (type, _args) => new ExpShareModifier(type)), - EXP_BALANCE: () => - new ModifierType( - "modifierType:ModifierType.EXP_BALANCE", - "exp_balance", - (type, _args) => new ExpBalanceModifier(type), - ), - - OVAL_CHARM: () => - new ModifierType( - "modifierType:ModifierType.OVAL_CHARM", - "oval_charm", - (type, _args) => new MultipleParticipantExpBonusModifier(type), - ), - - EXP_CHARM: () => new ExpBoosterModifierType("modifierType:ModifierType.EXP_CHARM", "exp_charm", 25), - SUPER_EXP_CHARM: () => new ExpBoosterModifierType("modifierType:ModifierType.SUPER_EXP_CHARM", "super_exp_charm", 60), - GOLDEN_EXP_CHARM: () => - new ExpBoosterModifierType("modifierType:ModifierType.GOLDEN_EXP_CHARM", "golden_exp_charm", 100), - - LUCKY_EGG: () => new PokemonExpBoosterModifierType("modifierType:ModifierType.LUCKY_EGG", "lucky_egg", 40), - GOLDEN_EGG: () => new PokemonExpBoosterModifierType("modifierType:ModifierType.GOLDEN_EGG", "golden_egg", 100), - - SOOTHE_BELL: () => new PokemonFriendshipBoosterModifierType("modifierType:ModifierType.SOOTHE_BELL", "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, - ]), - ), - - EVIOLITE: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.EVIOLITE", - "eviolite", - (type, args) => new EvolutionStatBoosterModifier(type, (args[0] as Pokemon).id, [Stat.DEF, Stat.SPDEF], 1.5), - ), - - SOUL_DEW: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.SOUL_DEW", - "soul_dew", - (type, args) => new PokemonNatureWeightModifier(type, (args[0] as Pokemon).id), - ), - - NUGGET: () => - new MoneyRewardModifierType( - "modifierType:ModifierType.NUGGET", - "nugget", - 1, - "modifierType:ModifierType.MoneyRewardModifierType.extra.small", - ), - BIG_NUGGET: () => - new MoneyRewardModifierType( - "modifierType:ModifierType.BIG_NUGGET", - "big_nugget", - 2.5, - "modifierType:ModifierType.MoneyRewardModifierType.extra.moderate", - ), - RELIC_GOLD: () => - new MoneyRewardModifierType( - "modifierType:ModifierType.RELIC_GOLD", - "relic_gold", - 10, - "modifierType:ModifierType.MoneyRewardModifierType.extra.large", - ), - - AMULET_COIN: () => - new ModifierType( - "modifierType:ModifierType.AMULET_COIN", - "amulet_coin", - (type, _args) => new MoneyMultiplierModifier(type), - ), - GOLDEN_PUNCH: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.GOLDEN_PUNCH", - "golden_punch", - (type, args) => new DamageMoneyRewardModifier(type, (args[0] as Pokemon).id), - ), - COIN_CASE: () => - new ModifierType( - "modifierType:ModifierType.COIN_CASE", - "coin_case", - (type, _args) => new MoneyInterestModifier(type), - ), - - LOCK_CAPSULE: () => - new ModifierType( - "modifierType:ModifierType.LOCK_CAPSULE", - "lock_capsule", - (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), - - MULTI_LENS: () => new PokemonMultiHitModifierType("modifierType:ModifierType.MULTI_LENS", "zoom_lens"), - - HEALING_CHARM: () => - new ModifierType( - "modifierType:ModifierType.HEALING_CHARM", - "healing_charm", - (type, _args) => new HealingBoosterModifier(type, 1.1), - ), - CANDY_JAR: () => - new ModifierType( - "modifierType:ModifierType.CANDY_JAR", - "candy_jar", - (type, _args) => new LevelIncrementBoosterModifier(type), - ), - - BERRY_POUCH: () => - new ModifierType( - "modifierType:ModifierType.BERRY_POUCH", - "berry_pouch", - (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), - ), - - QUICK_CLAW: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.QUICK_CLAW", - "quick_claw", - (type, args) => new BypassSpeedChanceModifier(type, (args[0] as Pokemon).id), - ), - - KINGS_ROCK: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.KINGS_ROCK", - "kings_rock", - (type, args) => new FlinchChanceModifier(type, (args[0] as Pokemon).id), - ), - - 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), - ), - - 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), - ), - - BATON: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.BATON", - "baton", - (type, args) => new SwitchEffectTransferModifier(type, (args[0] as Pokemon).id), - ), - - SHINY_CHARM: () => - new ModifierType( - "modifierType:ModifierType.SHINY_CHARM", - "shiny_charm", - (type, _args) => new ShinyRateBoosterModifier(type), - ), - ABILITY_CHARM: () => - new ModifierType( - "modifierType:ModifierType.ABILITY_CHARM", - "ability_charm", - (type, _args) => new HiddenAbilityRateBoosterModifier(type), - ), - CATCHING_CHARM: () => - new ModifierType( - "modifierType:ModifierType.CATCHING_CHARM", - "catching_charm", - (type, _args) => new CriticalCatchChanceBoosterModifier(type), - ), - - IV_SCANNER: () => - new ModifierType("modifierType:ModifierType.IV_SCANNER", "scanner", (type, _args) => new IvScannerModifier(type)), - - DNA_SPLICERS: () => new FusePokemonModifierType("modifierType:ModifierType.DNA_SPLICERS", "dna_splicers"), - - MINI_BLACK_HOLE: () => - new TurnHeldItemTransferModifierType("modifierType:ModifierType.MINI_BLACK_HOLE", "mini_black_hole"), - - VOUCHER: () => new AddVoucherModifierType(VoucherType.REGULAR, 1), - VOUCHER_PLUS: () => new AddVoucherModifierType(VoucherType.PLUS, 1), - VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1), - - GOLDEN_POKEBALL: () => - new ModifierType( - "modifierType:ModifierType.GOLDEN_POKEBALL", - "pb_gold", - (type, _args) => new ExtraModifierModifier(type), - undefined, - "se/pb_bounce_1", - ), - SILVER_POKEBALL: () => - new ModifierType( - "modifierType:ModifierType.SILVER_POKEBALL", - "pb_silver", - (type, _args) => new TempExtraModifierModifier(type, 100), - undefined, - "se/pb_bounce_1", - ), - - ENEMY_DAMAGE_BOOSTER: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_DAMAGE_BOOSTER", - "wl_item_drop", - (type, _args) => new EnemyDamageBoosterModifier(type, 5), - ), - ENEMY_DAMAGE_REDUCTION: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_DAMAGE_REDUCTION", - "wl_guard_spec", - (type, _args) => new EnemyDamageReducerModifier(type, 2.5), - ), - //ENEMY_SUPER_EFFECT_BOOSTER: () => new ModifierType('Type Advantage Token', 'Increases damage of super effective attacks by 30%', (type, _args) => new EnemySuperEffectiveDamageBoosterModifier(type, 30), 'wl_custom_super_effective'), - ENEMY_HEAL: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_HEAL", - "wl_potion", - (type, _args) => new EnemyTurnHealModifier(type, 2, 10), - ), - ENEMY_ATTACK_POISON_CHANCE: () => - new EnemyAttackStatusEffectChanceModifierType( - "modifierType:ModifierType.ENEMY_ATTACK_POISON_CHANCE", - "wl_antidote", - 5, - StatusEffect.POISON, - 10, - ), - ENEMY_ATTACK_PARALYZE_CHANCE: () => - new EnemyAttackStatusEffectChanceModifierType( - "modifierType:ModifierType.ENEMY_ATTACK_PARALYZE_CHANCE", - "wl_paralyze_heal", - 2.5, - StatusEffect.PARALYSIS, - 10, - ), - ENEMY_ATTACK_BURN_CHANCE: () => - new EnemyAttackStatusEffectChanceModifierType( - "modifierType:ModifierType.ENEMY_ATTACK_BURN_CHANCE", - "wl_burn_heal", - 5, - StatusEffect.BURN, - 10, - ), - ENEMY_STATUS_EFFECT_HEAL_CHANCE: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_STATUS_EFFECT_HEAL_CHANCE", - "wl_full_heal", - (type, _args) => new EnemyStatusEffectHealChanceModifier(type, 2.5, 10), - ), - ENEMY_ENDURE_CHANCE: () => - new EnemyEndureChanceModifierType("modifierType:ModifierType.ENEMY_ENDURE_CHANCE", "wl_reset_urge", 2), - ENEMY_FUSED_CHANCE: () => - new ModifierType( - "modifierType:ModifierType.ENEMY_FUSED_CHANCE", - "wl_custom_spliced", - (type, _args) => new EnemyFusionChanceModifier(type, 1), - ), - - MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () => - new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs) { - return new PokemonBaseStatTotalModifierType(pregenArgs[0] as 10 | -15); - } - return new PokemonBaseStatTotalModifierType(10); - }), - MYSTERY_ENCOUNTER_OLD_GATEAU: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU", - "old_gateau", - (type, args) => new PokemonBaseStatFlatModifier(type, (args[0] as Pokemon).id), - ), - MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => - new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs) { - return new ModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", - "black_sludge", - (type, _args) => new HealShopCostModifier(type, pregenArgs[0] as number), - ); - } - return new ModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", - "black_sludge", - (type, _args) => new HealShopCostModifier(type, 2.5), - ); - }), - MYSTERY_ENCOUNTER_MACHO_BRACE: () => - new PokemonHeldItemModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_MACHO_BRACE", - "macho_brace", - (type, args) => new PokemonIncrementingStatModifier(type, (args[0] as Pokemon).id), - ), - MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => - new ModifierType( - "modifierType:ModifierType.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET", - "golden_net", - (type, _args) => new BoostBugSpawnModifier(type), - ), -}); - -/** - * The initial set of modifier types, used to generate the modifier pool. - */ -export type ModifierTypes = typeof modifierTypeInitObj; - -export interface ModifierPool { - [tier: string]: WeightedModifierType[]; -} - -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. - */ -export const itemPoolChecks: Map = new Map(); - -export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: ModifierPoolType, rerollCount = 0) { - const pool = getModifierPoolForType(poolType); - itemPoolChecks.forEach((_v, k) => { - itemPoolChecks.set(k, false); - }); - - const ignoredIndexes = {}; - const modifierTableData = {}; - const thresholds = Object.fromEntries( - new Map( - Object.keys(pool).map(t => { - ignoredIndexes[t] = []; - const thresholds = new Map(); - const tierModifierIds: string[] = []; - let tierMaxWeight = 0; - let i = 0; - pool[t].reduce((total: number, modifierType: WeightedModifierType) => { - const weightedModifierType = modifierType as WeightedModifierType; - const existingModifiers = globalScene.findModifiers( - m => m.type.id === weightedModifierType.modifierType.id, - poolType === ModifierPoolType.PLAYER, - ); - const itemModifierType = - weightedModifierType.modifierType instanceof ModifierTypeGenerator - ? weightedModifierType.modifierType.generateType(party) - : weightedModifierType.modifierType; - const weight = - !existingModifiers.length || - itemModifierType instanceof PokemonHeldItemModifierType || - itemModifierType instanceof FormChangeItemModifierType || - existingModifiers.find(m => m.stackCount < m.getMaxStackCount(true)) - ? weightedModifierType.weight instanceof Function - ? // biome-ignore lint/complexity/noBannedTypes: TODO: refactor to not use Function type - (weightedModifierType.weight as Function)(party, rerollCount) - : (weightedModifierType.weight as number) - : 0; - 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; - } else { - ignoredIndexes[t].push(i++); - return total; - } - if (itemPoolChecks.has(modifierType.modifierType.id as ModifierTypeKeys)) { - itemPoolChecks.set(modifierType.modifierType.id as ModifierTypeKeys, true); - } - thresholds.set(total, i++); - return total; - }, 0); - for (const id of tierModifierIds) { - modifierTableData[id].tierPercent = Math.floor((modifierTableData[id].weight / tierMaxWeight) * 10000) / 100; - } - return [t, Object.fromEntries(thresholds)]; - }), - ), - ); - for (const id of Object.keys(modifierTableData)) { - modifierTableData[id].totalPercent = - Math.floor(modifierTableData[id].tierPercent * tierWeights[modifierTableData[id].tier] * 100) / 100; - modifierTableData[id].tier = ModifierTier[modifierTableData[id].tier]; - } - if (outputModifierData) { - console.table(modifierTableData); - } - switch (poolType) { - case ModifierPoolType.PLAYER: - modifierPoolThresholds = thresholds; - ignoredPoolIndexes = ignoredIndexes; - break; - case ModifierPoolType.WILD: - case ModifierPoolType.TRAINER: - enemyModifierPoolThresholds = thresholds; - enemyIgnoredPoolIndexes = ignoredIndexes; - break; - case ModifierPoolType.ENEMY_BUFF: - enemyBuffModifierPoolThresholds = thresholds; - enemyBuffIgnoredPoolIndexes = ignoredIndexes; - break; - case ModifierPoolType.DAILY_STARTER: - dailyStarterModifierPoolThresholds = thresholds; - ignoredDailyStarterPoolIndexes = ignoredIndexes; - break; - } -} - -export interface CustomModifierSettings { - /** If specified, will override the next X items to be the specified tier. These can upgrade with luck. */ - guaranteedModifierTiers?: ModifierTier[]; - /** If specified, will override the first X items to be specific modifier options (these should be pre-genned). */ - guaranteedModifierTypeOptions?: ModifierTypeOption[]; - /** If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). */ - guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; - /** - * 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 - * ```ts - * count = 4; - * customModifierSettings = { guaranteedModifierTiers: [ModifierTier.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 the value of `count`). - * @defaultValue `false` - */ - fillRemaining?: boolean; - /** 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. */ - rerollMultiplier?: number; - /** - * If `false`, will prevent set item tiers from upgrading via luck. - * @defaultValue `true` - */ - allowLuckUpgrades?: boolean; -} - -export function getModifierTypeFuncById(id: string): ModifierTypeFunc { - return modifierTypeInitObj[id]; -} - -/** - * Generates modifier options for a {@linkcode SelectModifierPhase} - * @param count - Determines the number of items to generate - * @param party - Party is required for generating proper modifier pools - * @param modifierTiers - (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule. - * @param customModifierSettings - See {@linkcode CustomModifierSettings} - */ -export function getPlayerModifierTypeOptions( - count: number, - party: PlayerPokemon[], - modifierTiers?: ModifierTier[], - customModifierSettings?: CustomModifierSettings, -): ModifierTypeOption[] { - const options: ModifierTypeOption[] = []; - const retryCount = Math.min(count * 5, 50); - if (!customModifierSettings) { - for (let i = 0; i < count; i++) { - const tier = modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined; - options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier)); - } - } else { - // Guaranteed mod options first - if ( - customModifierSettings?.guaranteedModifierTypeOptions && - customModifierSettings.guaranteedModifierTypeOptions.length > 0 - ) { - options.push(...customModifierSettings.guaranteedModifierTypeOptions!); - } - - // Guaranteed mod functions second - if ( - customModifierSettings.guaranteedModifierTypeFuncs && - customModifierSettings.guaranteedModifierTypeFuncs.length > 0 - ) { - customModifierSettings.guaranteedModifierTypeFuncs!.forEach((mod, _i) => { - const modifierId = Object.keys(modifierTypeInitObj).find(k => modifierTypeInitObj[k] === mod) as string; - let guaranteedMod: ModifierType = modifierTypeInitObj[modifierId]?.(); - - // Populates item id and tier - guaranteedMod = guaranteedMod - .withIdFromFunc(modifierTypeInitObj[modifierId]) - .withTierFromPool(ModifierPoolType.PLAYER, party); - - const modType = - guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; - if (modType) { - const option = new ModifierTypeOption(modType, 0); - options.push(option); - } - }); - } - - // Guaranteed tiers third - if (customModifierSettings.guaranteedModifierTiers && customModifierSettings.guaranteedModifierTiers.length > 0) { - const allowLuckUpgrades = customModifierSettings.allowLuckUpgrades ?? true; - for (const tier of customModifierSettings.guaranteedModifierTiers) { - options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier, allowLuckUpgrades)); - } - } - - // Fill remaining - if (options.length < count && customModifierSettings.fillRemaining) { - while (options.length < count) { - options.push(getModifierTypeOptionWithRetry(options, retryCount, party, undefined)); - } - } - } - - overridePlayerModifierTypeOptions(options, party); - - return options; -} - -/** - * Will generate a {@linkcode ModifierType} from the {@linkcode ModifierPoolType.PLAYER} pool, attempting to retry duplicated items up to retryCount - * @param existingOptions Currently generated options - * @param retryCount How many times to retry before allowing a dupe item - * @param party Current player party, used to calculate items in the pool - * @param tier If specified will generate item of tier - * @param allowLuckUpgrades `true` to allow items to upgrade tiers (the little animation that plays and is affected by luck) - */ -function getModifierTypeOptionWithRetry( - existingOptions: ModifierTypeOption[], - retryCount: number, - party: PlayerPokemon[], - tier?: ModifierTier, - allowLuckUpgrades?: boolean, -): ModifierTypeOption { - allowLuckUpgrades = allowLuckUpgrades ?? true; - let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); - let r = 0; - while ( - existingOptions.length && - ++r < retryCount && - existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length - ) { - candidate = getNewModifierTypeOption( - party, - ModifierPoolType.PLAYER, - candidate?.type.tier ?? tier, - candidate?.upgradeCount, - 0, - allowLuckUpgrades, - ); - } - return candidate!; -} - -/** - * Replaces the {@linkcode ModifierType} of the entries within {@linkcode options} with any - * {@linkcode ModifierOverride} entries listed in {@linkcode Overrides.ITEM_REWARD_OVERRIDE} - * up to the smallest amount of entries between {@linkcode options} and the override array. - * @param options Array of naturally rolled {@linkcode ModifierTypeOption}s - * @param party Array of the player's current party - */ -export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[], party: PlayerPokemon[]) { - const minLength = Math.min(options.length, Overrides.ITEM_REWARD_OVERRIDE.length); - for (let i = 0; i < minLength; i++) { - const override: ModifierOverride = Overrides.ITEM_REWARD_OVERRIDE[i]; - const modifierFunc = modifierTypeInitObj[override.name]; - let modifierType: ModifierType | null = modifierFunc(); - - if (modifierType instanceof ModifierTypeGenerator) { - const pregenArgs = "type" in override && override.type !== null ? [override.type] : undefined; - modifierType = modifierType.generateType(party, pregenArgs); - } - - if (modifierType) { - options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(ModifierPoolType.PLAYER, party); - } - } -} - -export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseCost: number): ModifierTypeOption[] { - if (!(waveIndex % 10)) { - return []; - } - - const options = [ - [ - new ModifierTypeOption(modifierTypeInitObj.POTION(), 0, baseCost * 0.2), - new ModifierTypeOption(modifierTypeInitObj.ETHER(), 0, baseCost * 0.4), - new ModifierTypeOption(modifierTypeInitObj.REVIVE(), 0, baseCost * 2), - ], - [ - new ModifierTypeOption(modifierTypeInitObj.SUPER_POTION(), 0, baseCost * 0.45), - new ModifierTypeOption(modifierTypeInitObj.FULL_HEAL(), 0, baseCost), - ], - [ - new ModifierTypeOption(modifierTypeInitObj.ELIXIR(), 0, baseCost), - new ModifierTypeOption(modifierTypeInitObj.MAX_ETHER(), 0, baseCost), - ], - [ - new ModifierTypeOption(modifierTypeInitObj.HYPER_POTION(), 0, baseCost * 0.8), - new ModifierTypeOption(modifierTypeInitObj.MAX_REVIVE(), 0, baseCost * 2.75), - new ModifierTypeOption(modifierTypeInitObj.MEMORY_MUSHROOM(), 0, baseCost * 4), - ], - [ - new ModifierTypeOption(modifierTypeInitObj.MAX_POTION(), 0, baseCost * 1.5), - new ModifierTypeOption(modifierTypeInitObj.MAX_ELIXIR(), 0, baseCost * 2.5), - ], - [new ModifierTypeOption(modifierTypeInitObj.FULL_RESTORE(), 0, baseCost * 2.25)], - [new ModifierTypeOption(modifierTypeInitObj.SACRED_ASH(), 0, baseCost * 10)], - ]; - return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat(); -} - -export function getEnemyBuffModifierForWave( - tier: ModifierTier, - enemyModifiers: PersistentModifier[], -): EnemyPersistentModifier { - let tierStackCount: number; - switch (tier) { - case ModifierTier.ULTRA: - tierStackCount = 5; - break; - case ModifierTier.GREAT: - tierStackCount = 3; - break; - default: - tierStackCount = 1; - break; - } - - const retryCount = 50; - let candidate = getNewModifierTypeOption([], ModifierPoolType.ENEMY_BUFF, tier); - let r = 0; - let matchingModifier: PersistentModifier | undefined; - while ( - ++r < retryCount && - (matchingModifier = enemyModifiers.find(m => m.type.id === candidate?.type?.id)) && - matchingModifier.getMaxStackCount() < matchingModifier.stackCount + (r < 10 ? tierStackCount : 1) - ) { - candidate = getNewModifierTypeOption([], ModifierPoolType.ENEMY_BUFF, tier); - } - - const modifier = candidate?.type?.newModifier() as EnemyPersistentModifier; - modifier.stackCount = tierStackCount; - - return modifier; -} - -export function getEnemyModifierTypesForWave( - waveIndex: number, - count: number, - party: EnemyPokemon[], - poolType: ModifierPoolType.WILD | ModifierPoolType.TRAINER, - upgradeChance = 0, -): PokemonHeldItemModifierType[] { - const ret = new Array(count) - .fill(0) - .map( - () => - getNewModifierTypeOption(party, poolType, undefined, upgradeChance && !randSeedInt(upgradeChance) ? 1 : 0) - ?.type as PokemonHeldItemModifierType, - ); - if (!(waveIndex % 1000)) { - ret.push(getModifierType(modifierTypeInitObj.MINI_BLACK_HOLE) as PokemonHeldItemModifierType); - } - return ret; -} - -export function getDailyRunStarterModifiers(party: PlayerPokemon[]): PokemonHeldItemModifier[] { - const ret: PokemonHeldItemModifier[] = []; - for (const p of party) { - for (let m = 0; m < 3; m++) { - const tierValue = randSeedInt(64); - - let tier: ModifierTier; - if (tierValue > 25) { - tier = ModifierTier.COMMON; - } else if (tierValue > 12) { - tier = ModifierTier.GREAT; - } else if (tierValue > 4) { - tier = ModifierTier.ULTRA; - } else if (tierValue) { - tier = ModifierTier.ROGUE; - } else { - tier = ModifierTier.MASTER; - } - - const modifier = getNewModifierTypeOption(party, ModifierPoolType.DAILY_STARTER, tier)?.type?.newModifier( - p, - ) as PokemonHeldItemModifier; - ret.push(modifier); - } - } - - return ret; -} - -/** - * Generates a ModifierType from the specified pool - * @param party party of the trainer using the item - * @param poolType PLAYER/WILD/TRAINER - * @param tier If specified, will override the initial tier of an item (can still upgrade with luck) - * @param upgradeCount If defined, means that this is a new ModifierType being generated to override another via luck upgrade. Used for recursive logic - * @param retryCount Max allowed tries before the next tier down is checked for a valid ModifierType - * @param allowLuckUpgrades Default true. If false, will not allow ModifierType to randomly upgrade to next tier - */ -function getNewModifierTypeOption( - party: Pokemon[], - poolType: ModifierPoolType, - tier?: ModifierTier, - 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); - } - - 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 tierThresholds = Object.keys(thresholds[tier]); - const totalWeight = Number.parseInt(tierThresholds[tierThresholds.length - 1]); - const value = randSeedInt(totalWeight); - let index: number | undefined; - for (const t of tierThresholds) { - const threshold = Number.parseInt(t); - if (value < threshold) { - index = thresholds[tier][threshold]; - break; - } - } - - if (index === undefined) { - return null; - } - - if (player) { - console.log(index, ignoredPoolIndexes[tier].filter(i => i <= index).length, ignoredPoolIndexes[tier]); - } - let modifierType: ModifierType | null = pool[tier][index].modifierType; - if (modifierType instanceof ModifierTypeGenerator) { - modifierType = (modifierType as ModifierTypeGenerator).generateType(party); - if (modifierType === null) { - if (player) { - console.log(ModifierTier[tier], upgradeCount); - } - return getNewModifierTypeOption(party, poolType, tier, upgradeCount, ++retryCount); - } - } - - console.log(modifierType, !player ? "(enemy)" : ""); - - return new ModifierTypeOption(modifierType as ModifierType, upgradeCount!); // TODO: is this bang correct? -} - -export function getDefaultModifierTypeForTier(tier: ModifierTier): ModifierType { - const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER); - let modifierType: ModifierType | WeightedModifierType = modifierPool[tier || ModifierTier.COMMON][0]; - if (modifierType instanceof WeightedModifierType) { - modifierType = (modifierType as WeightedModifierType).modifierType; - } - return modifierType; -} - -export class ModifierTypeOption { - public type: ModifierType; - public upgradeCount: number; - public cost: number; - - constructor(type: ModifierType, upgradeCount: number, cost = 0) { - this.type = type; - this.upgradeCount = upgradeCount; - this.cost = Math.min(Math.round(cost), Number.MAX_SAFE_INTEGER); - } -} - -/** - * Calculates the team's luck value. - * @param party The player's party. - * @returns A number between 0 and 14 based on the party's total luck value, or a random number between 0 and 14 if the player is in Daily Run mode. - */ -export function getPartyLuckValue(party: Pokemon[]): number { - if (globalScene.gameMode.isDaily) { - const DailyLuck = new NumberHolder(0); - globalScene.executeWithSeedOffset( - () => { - DailyLuck.value = randSeedInt(15); // Random number between 0 and 14 - }, - 0, - globalScene.seed, - ); - return DailyLuck.value; - } - const eventSpecies = timedEventManager.getEventLuckBoostedSpecies(); - const luck = Phaser.Math.Clamp( - party - .map(p => (p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0)) - .reduce((total: number, value: number) => (total += value), 0), - 0, - 14, - ); - return Math.min(timedEventManager.getEventLuckBoost() + (luck ?? 0), 14); -} - -export function getLuckString(luckValue: number): string { - return ["D", "C", "C+", "B-", "B", "B+", "A-", "A", "A+", "A++", "S", "S+", "SS", "SS+", "SSS"][luckValue]; -} - -export function getLuckTextTint(luckValue: number): number { - let modifierTier: ModifierTier; - if (luckValue > 11) { - modifierTier = ModifierTier.LUXURY; - } else if (luckValue > 9) { - modifierTier = ModifierTier.MASTER; - } else if (luckValue > 5) { - modifierTier = ModifierTier.ROGUE; - } else if (luckValue > 2) { - modifierTier = ModifierTier.ULTRA; - } else if (luckValue) { - modifierTier = ModifierTier.GREAT; - } else { - modifierTier = ModifierTier.COMMON; - } - return getModifierTierTextTint(modifierTier); -} - -export function initModifierTypes() { - for (const [key, value] of Object.entries(modifierTypeInitObj)) { - modifierTypes[key] = value; - } -} - -// TODO: If necessary, add the rest of the modifier types here. -// For now, doing the minimal work until the modifier rework lands. -const ModifierTypeConstructorMap = Object.freeze({ - ModifierTypeGenerator, - PokemonHeldItemModifierType, -}); - -/** - * Map of of modifier type strings to their constructor type - */ -export type ModifierTypeConstructorMap = typeof ModifierTypeConstructorMap; - -/** - * Map of modifier type strings to their instance type - */ -export type ModifierTypeInstanceMap = { - [K in keyof ModifierTypeConstructorMap]: InstanceType; -}; - -export type ModifierTypeString = keyof ModifierTypeConstructorMap; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts deleted file mode 100644 index b31bee7fc69..00000000000 --- a/src/modifier/modifier.ts +++ /dev/null @@ -1,3930 +0,0 @@ -import { applyAbAttrs } from "#abilities/apply-ab-attrs"; -import { globalScene } from "#app/global-scene"; -import { getPokemonNameWithAffix } from "#app/messages"; -import Overrides from "#app/overrides"; -import { FusionSpeciesFormEvolution, pokemonEvolutions } from "#balance/pokemon-evolutions"; -import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#balance/starters"; -import { getBerryEffectFunc, getBerryPredicate } from "#data/berry"; -import { allMoves, modifierTypes } from "#data/data-lists"; -import { getLevelTotalExp } from "#data/exp"; -import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; -import { MAX_PER_TYPE_POKEBALLS } from "#data/pokeball"; -import { getStatusEffectHealText } from "#data/status-effect"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; -import { Color, ShadowColor } from "#enums/color"; -import { Command } from "#enums/command"; -import type { FormChangeItem } from "#enums/form-change-item"; -import { LearnMoveType } from "#enums/learn-move-type"; -import type { MoveId } from "#enums/move-id"; -import type { Nature } from "#enums/nature"; -import type { PokeballType } from "#enums/pokeball"; -import type { PokemonType } from "#enums/pokemon-type"; -import { SpeciesId } from "#enums/species-id"; -import { BATTLE_STATS, type PermanentStat, Stat, TEMP_BATTLE_STATS, type TempBattleStat } from "#enums/stat"; -import { StatusEffect } from "#enums/status-effect"; -import { TextStyle } from "#enums/text-style"; -import type { PlayerPokemon, Pokemon } from "#field/pokemon"; -import type { - DoubleBattleChanceBoosterModifierType, - EvolutionItemModifierType, - FormChangeItemModifierType, - ModifierOverride, - ModifierType, - PokemonBaseStatTotalModifierType, - PokemonExpBoosterModifierType, - PokemonFriendshipBoosterModifierType, - PokemonMoveAccuracyBoosterModifierType, - PokemonMultiHitModifierType, - TerastallizeModifierType, - TmModifierType, -} from "#modifiers/modifier-type"; -import type { VoucherType } from "#system/voucher"; -import type { ModifierInstanceMap, ModifierString } from "#types/modifier-types"; -import { addTextObject } from "#ui/text"; -import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common"; -import { getModifierType } from "#utils/modifier-utils"; -import i18next from "i18next"; - -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 : -1; - const bId = b instanceof PokemonHeldItemModifier ? b.pokemonId : -1; - - // First sort by pokemon ID, then by item type and then name - return aId - bId || typeNameMatch || itemNameMatch; -}; - -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; - - constructor(type: ModifierType) { - this.type = type; - } - - /** - * Return whether this modifier is of the given class - * - * @remarks - * Used to avoid requiring the caller to have imported the specific modifier class, avoiding circular dependencies. - * - * @param modifier - The modifier to check against - * @returns Whether the modiifer is an instance of the given type - */ - public is(modifier: T): this is ModifierInstanceMap[T] { - const targetModifier = ModifierClassMap[modifier]; - if (!targetModifier) { - return false; - } - return this instanceof targetModifier; - } - - match(_modifier: Modifier): boolean { - return false; - } - - /** - * Checks if {@linkcode Modifier} should be applied - * @param _args parameters passed to {@linkcode Modifier.apply} - * @returns always `true` by default - */ - shouldApply(..._args: Parameters): boolean { - return true; - } - - /** - * Handles applying of {@linkcode Modifier} - * @param args collection of all passed parameters - */ - abstract apply(...args: unknown[]): boolean; -} - -export abstract class PersistentModifier extends Modifier { - public stackCount: number; - public virtualStackCount: number; - - /** This field does not exist at runtime and must not be used. - * Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called. - */ - private declare _: never; - - constructor(type: ModifierType, stackCount = 1) { - super(type); - this.stackCount = stackCount; - this.virtualStackCount = 0; - } - - add(modifiers: PersistentModifier[], virtual: boolean): boolean { - for (const modifier of modifiers) { - if (this.match(modifier)) { - return modifier.incrementStack(this.stackCount, virtual); - } - } - - if (virtual) { - this.virtualStackCount += this.stackCount; - this.stackCount = 0; - } - modifiers.push(this); - return true; - } - - abstract clone(): PersistentModifier; - - getArgs(): any[] { - return []; - } - - incrementStack(amount: number, virtual: boolean): boolean { - if (this.getStackCount() + amount <= this.getMaxStackCount()) { - if (!virtual) { - this.stackCount += amount; - } else { - this.virtualStackCount += amount; - } - return true; - } - - return false; - } - - getStackCount(): number { - return this.stackCount + this.virtualStackCount; - } - - abstract getMaxStackCount(forThreshold?: boolean): number; - - getCountUnderMax(): number { - return this.getMaxStackCount() - this.getStackCount(); - } - - isIconVisible(): boolean { - return true; - } - - getIcon(_forSummary?: boolean): Phaser.GameObjects.Container { - const container = globalScene.add.container(0, 0); - - const item = globalScene.add.sprite(0, 12, "items"); - item.setFrame(this.type.iconImage); - item.setOrigin(0, 0.5); - container.add(item); - - const stackText = this.getIconStackText(); - if (stackText) { - container.add(stackText); - } - - const virtualStackText = this.getIconStackText(true); - if (virtualStackText) { - container.add(virtualStackText); - } - - return container; - } - - getIconStackText(virtual?: boolean): Phaser.GameObjects.BitmapText | null { - if (this.getMaxStackCount() === 1 || (virtual && !this.virtualStackCount)) { - return null; - } - - const text = globalScene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); - text.letterSpacing = -0.5; - if (this.getStackCount() >= this.getMaxStackCount()) { - text.setTint(0xf89890); - } - text.setOrigin(0, 0); - - return text; - } -} - -export abstract class ConsumableModifier extends Modifier { - add(_modifiers: Modifier[]): boolean { - return true; - } -} - -export class AddPokeballModifier extends ConsumableModifier { - private pokeballType: PokeballType; - private count: number; - - constructor(type: ModifierType, pokeballType: PokeballType, count: number) { - super(type); - - this.pokeballType = pokeballType; - this.count = count; - } - - /** - * Applies {@linkcode AddPokeballModifier} - * @param battleScene {@linkcode BattleScene} - * @returns always `true` - */ - override apply(): boolean { - const pokeballCounts = globalScene.pokeballCounts; - pokeballCounts[this.pokeballType] = Math.min( - pokeballCounts[this.pokeballType] + this.count, - MAX_PER_TYPE_POKEBALLS, - ); - - return true; - } -} - -export class AddVoucherModifier extends ConsumableModifier { - private voucherType: VoucherType; - private count: number; - - constructor(type: ModifierType, voucherType: VoucherType, count: number) { - super(type); - - this.voucherType = voucherType; - this.count = count; - } - - /** - * Applies {@linkcode AddVoucherModifier} - * @param battleScene {@linkcode BattleScene} - * @returns always `true` - */ - override apply(): boolean { - const voucherCounts = globalScene.gameData.voucherCounts; - voucherCounts[this.voucherType] += this.count; - - return true; - } -} - -/** - * Modifier used for party-wide or passive items that start an initial - * {@linkcode battleCount} equal to {@linkcode maxBattles} that, for every - * battle, decrements. Typically, when {@linkcode battleCount} reaches 0, the - * modifier will be removed. If a modifier of the same type is to be added, it - * will reset {@linkcode battleCount} back to {@linkcode maxBattles} of the - * existing modifier instead of adding that modifier directly. - * @extends PersistentModifier - * @abstract - * @see {@linkcode add} - */ -export abstract class LapsingPersistentModifier extends PersistentModifier { - /** The maximum amount of battles the modifier will exist for */ - private maxBattles: number; - /** The current amount of battles the modifier will exist for */ - private battleCount: number; - - constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) { - super(type, stackCount); - - this.maxBattles = maxBattles; - this.battleCount = battleCount ?? this.maxBattles; - } - - /** - * Goes through existing modifiers for any that match the selected modifier, - * which will then either add it to the existing modifiers if none were found - * or, if one was found, it will refresh {@linkcode battleCount}. - * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers - * @param _virtual N/A - * @param _scene N/A - * @returns `true` if the modifier was successfully added or applied, false otherwise - */ - add(modifiers: PersistentModifier[], _virtual: boolean): boolean { - for (const modifier of modifiers) { - if (this.match(modifier)) { - const modifierInstance = modifier as LapsingPersistentModifier; - if (modifierInstance.getBattleCount() < modifierInstance.getMaxBattles()) { - modifierInstance.resetBattleCount(); - globalScene.playSound("se/restore"); - return true; - } - // should never get here - return false; - } - } - - modifiers.push(this); - return true; - } - - /** - * Lapses the {@linkcode battleCount} by 1. - * @param _args passed arguments (not in use here) - * @returns `true` if the {@linkcode battleCount} is greater than 0 - */ - public lapse(..._args: unknown[]): boolean { - this.battleCount--; - return this.battleCount > 0; - } - - getIcon(): Phaser.GameObjects.Container { - const container = super.getIcon(); - - // Linear interpolation on hue - const hue = Math.floor(120 * (this.battleCount / this.maxBattles) + 5); - - // Generates the color hex code with a constant saturation and lightness but varying hue - const typeHex = hslToHex(hue, 0.5, 0.9); - const strokeHex = hslToHex(hue, 0.7, 0.3); - - const battleCountText = addTextObject(27, 0, this.battleCount.toString(), TextStyle.PARTY, { - fontSize: "66px", - color: typeHex, - }); - battleCountText.setShadow(0, 0); - battleCountText.setStroke(strokeHex, 16); - battleCountText.setOrigin(1, 0); - container.add(battleCountText); - - return container; - } - - getIconStackText(_virtual?: boolean): Phaser.GameObjects.BitmapText | null { - return null; - } - - getBattleCount(): number { - return this.battleCount; - } - - resetBattleCount(): void { - this.battleCount = this.maxBattles; - } - - /** - * Updates an existing modifier with a new `maxBattles` and `battleCount`. - */ - setNewBattleCount(count: number): void { - this.maxBattles = count; - this.battleCount = count; - } - - getMaxBattles(): number { - return this.maxBattles; - } - - getArgs(): any[] { - return [this.maxBattles, this.battleCount]; - } - - getMaxStackCount(_forThreshold?: boolean): number { - // Must be an abitrary number greater than 1 - return 2; - } -} - -/** - * Modifier used for passive items, specifically lures, that - * temporarily increases the chance of a double battle. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} - */ -export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier { - public declare type: DoubleBattleChanceBoosterModifierType; - - match(modifier: Modifier): boolean { - return modifier instanceof DoubleBattleChanceBoosterModifier && modifier.getMaxBattles() === this.getMaxBattles(); - } - - clone(): DoubleBattleChanceBoosterModifier { - return new DoubleBattleChanceBoosterModifier( - this.type, - this.getMaxBattles(), - this.getBattleCount(), - this.stackCount, - ); - } - - /** - * Increases the chance of a double battle occurring - * @param doubleBattleChance {@linkcode NumberHolder} for double battle chance - * @returns true - */ - override apply(doubleBattleChance: NumberHolder): boolean { - // This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using randSeedInt - // A double battle will initiate if the generated number is 0 - doubleBattleChance.value = doubleBattleChance.value / 4; - - return true; - } -} - -/** - * Modifier used for party-wide items, specifically the X items, that - * temporarily increases the stat stage multiplier of the corresponding - * {@linkcode TempBattleStat}. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} - */ -export class TempStatStageBoosterModifier extends LapsingPersistentModifier { - /** The stat whose stat stage multiplier will be temporarily increased */ - private stat: TempBattleStat; - /** The amount by which the stat stage itself or its multiplier will be increased by */ - private boost: number; - - constructor(type: ModifierType, stat: TempBattleStat, maxBattles: number, battleCount?: number, stackCount?: number) { - super(type, maxBattles, battleCount, stackCount); - - this.stat = stat; - // Note that, because we want X Accuracy to maintain its original behavior, - // it will increment as it did previously, directly to the stat stage. - this.boost = stat !== Stat.ACC ? 0.3 : 1; - } - - match(modifier: Modifier): boolean { - if (modifier instanceof TempStatStageBoosterModifier) { - const modifierInstance = modifier as TempStatStageBoosterModifier; - return modifierInstance.stat === this.stat; - } - return false; - } - - clone() { - return new TempStatStageBoosterModifier( - this.type, - this.stat, - this.getMaxBattles(), - this.getBattleCount(), - this.stackCount, - ); - } - - getArgs(): any[] { - return [this.stat, ...super.getArgs()]; - } - - /** - * Checks if {@linkcode args} contains the necessary elements and if the - * incoming stat is matches {@linkcode stat}. - * @param tempBattleStat {@linkcode TempBattleStat} being affected - * @param statLevel {@linkcode NumberHolder} that holds the resulting value of the stat stage multiplier - * @returns `true` if the modifier can be applied, false otherwise - */ - override shouldApply(tempBattleStat?: TempBattleStat, statLevel?: NumberHolder): boolean { - return ( - !!tempBattleStat && !!statLevel && TEMP_BATTLE_STATS.includes(tempBattleStat) && tempBattleStat === this.stat - ); - } - - /** - * Increases the incoming stat stage matching {@linkcode stat} by {@linkcode boost}. - * @param _tempBattleStat {@linkcode TempBattleStat} N/A - * @param statLevel {@linkcode NumberHolder} that holds the resulting value of the stat stage multiplier - */ - override apply(_tempBattleStat: TempBattleStat, statLevel: NumberHolder): boolean { - statLevel.value += this.boost; - return true; - } -} - -/** - * Modifier used for party-wide items, namely Dire Hit, that - * temporarily increments the critical-hit stage - * @extends LapsingPersistentModifier - * @see {@linkcode apply} - */ -export class TempCritBoosterModifier extends LapsingPersistentModifier { - clone() { - return new TempCritBoosterModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount); - } - - match(modifier: Modifier): boolean { - return modifier instanceof TempCritBoosterModifier; - } - - /** - * Checks if {@linkcode args} contains the necessary elements. - * @param critLevel {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns `true` if the critical-hit stage boost applies successfully - */ - override shouldApply(critLevel?: NumberHolder): boolean { - return !!critLevel; - } - - /** - * Increases the current critical-hit stage value by 1. - * @param critLevel {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns `true` if the critical-hit stage boost applies successfully - */ - override apply(critLevel: NumberHolder): boolean { - critLevel.value++; - return true; - } -} - -export class MapModifier extends PersistentModifier { - clone(): MapModifier { - return new MapModifier(this.type, this.stackCount); - } - - override apply(..._args: unknown[]): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export class MegaEvolutionAccessModifier extends PersistentModifier { - clone(): MegaEvolutionAccessModifier { - return new MegaEvolutionAccessModifier(this.type, this.stackCount); - } - - override apply(..._args: unknown[]): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export class GigantamaxAccessModifier extends PersistentModifier { - clone(): GigantamaxAccessModifier { - return new GigantamaxAccessModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode GigantamaxAccessModifier} - * @param _args N/A - * @returns always `true` - */ - apply(..._args: unknown[]): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export class TerastallizeAccessModifier extends PersistentModifier { - clone(): TerastallizeAccessModifier { - return new TerastallizeAccessModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode TerastallizeAccessModifier} - * @param _args N/A - * @returns always `true` - */ - override apply(..._args: unknown[]): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export abstract class PokemonHeldItemModifier extends PersistentModifier { - /** The ID of the {@linkcode Pokemon} that this item belongs to. */ - public pokemonId: number; - /** Whether this item can be transfered to or stolen by another Pokemon. */ - public isTransferable = true; - - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, stackCount); - - this.pokemonId = pokemonId; - } - - abstract matchType(_modifier: Modifier): boolean; - - match(modifier: Modifier) { - return this.matchType(modifier) && (modifier as PokemonHeldItemModifier).pokemonId === this.pokemonId; - } - - getArgs(): any[] { - return [this.pokemonId]; - } - - /** - * Applies the {@linkcode PokemonHeldItemModifier} to the given {@linkcode Pokemon}. - * @param pokemon The {@linkcode Pokemon} that holds the held item - * @param args additional parameters - */ - abstract override apply(pokemon: Pokemon, ...args: unknown[]): boolean; - - /** - * Checks if {@linkcode PokemonHeldItemModifier} should be applied. - * @param pokemon The {@linkcode Pokemon} that holds the item - * @param _args N/A - * @returns if {@linkcode PokemonHeldItemModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, ..._args: unknown[]): boolean { - return !!pokemon && (this.pokemonId === -1 || pokemon.id === this.pokemonId); - } - - isIconVisible(): boolean { - return !!this.getPokemon()?.isOnField(); - } - - getIcon(forSummary?: boolean): Phaser.GameObjects.Container { - const container = !forSummary ? globalScene.add.container(0, 0) : super.getIcon(); - - if (!forSummary) { - const pokemon = this.getPokemon(); - if (pokemon) { - const pokemonIcon = globalScene.addPokemonIcon(pokemon, -2, 10, 0, 0.5, undefined, true); - container.add(pokemonIcon); - container.setName(pokemon.id.toString()); - } - - const item = globalScene.add.sprite(16, this.virtualStackCount ? 8 : 16, "items"); - item.setScale(0.5); - item.setOrigin(0, 0.5); - item.setTexture("items", this.type.iconImage); - container.add(item); - - const stackText = this.getIconStackText(); - if (stackText) { - container.add(stackText); - } - - const virtualStackText = this.getIconStackText(true); - if (virtualStackText) { - container.add(virtualStackText); - } - } else { - container.setScale(0.5); - } - - return container; - } - - getPokemon(): Pokemon | undefined { - return globalScene.getPokemonById(this.pokemonId) ?? undefined; - } - - getScoreMultiplier(): number { - return 1; - } - - getMaxStackCount(forThreshold = false): 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 declare type: PokemonBaseStatTotalModifierType; - public isTransferable = false; - public statModifier: 10 | -15; - - constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: 10 | -15, stackCount?: number) { - super(type, pokemonId, stackCount); - this.statModifier = statModifier; - } - - override matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonBaseStatTotalModifier && this.statModifier === modifier.statModifier; - } - - override clone(): PersistentModifier { - return new PokemonBaseStatTotalModifier(this.type, this.pokemonId, this.statModifier, this.stackCount); - } - - override getArgs(): any[] { - return super.getArgs().concat(this.statModifier); - } - - /** - * Checks if {@linkcode PokemonBaseStatTotalModifier} should be applied to the specified {@linkcode Pokemon}. - * @param pokemon the {@linkcode Pokemon} to be modified - * @param baseStats the base stats of the {@linkcode Pokemon} - * @returns `true` if the {@linkcode Pokemon} should be modified - */ - override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { - return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); - } - - /** - * Applies the {@linkcode PokemonBaseStatTotalModifier} - * @param _pokemon the {@linkcode Pokemon} to be modified - * @param baseStats the base stats of the {@linkcode Pokemon} - * @returns always `true` - */ - override apply(_pokemon: Pokemon, baseStats: number[]): boolean { - // Modifies the passed in baseStats[] array - baseStats.forEach((v, i) => { - // HP is affected by half as much as other stats - const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier); - baseStats[i] = Math.min(Math.max(newVal, 1), 999999); - }); - - return true; - } - - override getScoreMultiplier(): number { - return 1.2; - } - - override getMaxHeldItemCount(_pokemon: Pokemon): number { - return 2; - } -} - -/** - * Currently used by Old Gateau item - */ -export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { - public isTransferable = false; - - override matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonBaseStatFlatModifier; - } - - override clone(): PersistentModifier { - return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if the {@linkcode PokemonBaseStatFlatModifier} should be applied to the {@linkcode Pokemon}. - * @param pokemon The {@linkcode Pokemon} that holds the item - * @param baseStats The base stats of the {@linkcode Pokemon} - * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { - return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); - } - - /** - * Applies the {@linkcode PokemonBaseStatFlatModifier} - * @param _pokemon The {@linkcode Pokemon} that holds the item - * @param baseStats The base stats of the {@linkcode Pokemon} - * @returns always `true` - */ - override apply(pokemon: Pokemon, baseStats: number[]): boolean { - // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats - const stats = this.getStats(pokemon); - const statModifier = 20; - baseStats.forEach((v, i) => { - if (stats.includes(i)) { - const newVal = Math.floor(v + statModifier); - baseStats[i] = Math.min(Math.max(newVal, 1), 999999); - } - }); - - return true; - } - - /** - * Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef - * @returns Array of 3 {@linkcode Stat}s to boost - */ - getStats(pokemon: Pokemon): Stat[] { - const stats: Stat[] = []; - const baseStats = pokemon.getSpeciesForm().baseStats.slice(0); - // HP or Speed - stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD); - // Attack or SpAtk - stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); - // Def or SpDef - stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); - return stats; - } - - override getScoreMultiplier(): number { - return 1.1; - } - - override getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Currently used by Macho Brace item - */ -export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { - public isTransferable = false; - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonIncrementingStatModifier; - } - - clone(): PokemonIncrementingStatModifier { - return new PokemonIncrementingStatModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if the {@linkcode PokemonIncrementingStatModifier} should be applied to the {@linkcode Pokemon}. - * @param pokemon The {@linkcode Pokemon} that holds the item - * @param stat The affected {@linkcode Stat} - * @param statHolder The {@linkcode NumberHolder} that holds the stat - * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, stat?: Stat, statHolder?: NumberHolder): boolean { - return super.shouldApply(pokemon, stat, statHolder) && !!statHolder; - } - - /** - * Applies the {@linkcode PokemonIncrementingStatModifier} - * @param _pokemon The {@linkcode Pokemon} that holds the item - * @param stat The affected {@linkcode Stat} - * @param statHolder The {@linkcode NumberHolder} that holds the stat - * @returns always `true` - */ - override apply(_pokemon: Pokemon, stat: Stat, statHolder: NumberHolder): boolean { - // Modifies the passed in stat number holder by +2 per stack for HP, +1 per stack for other stats - // If the Macho Brace is at max stacks (50), adds additional 10% to total HP and 5% to other stats - const isHp = stat === Stat.HP; - - if (isHp) { - statHolder.value += 2 * this.stackCount; - if (this.stackCount === this.getMaxHeldItemCount()) { - statHolder.value = Math.floor(statHolder.value * 1.1); - } - } else { - statHolder.value += this.stackCount; - if (this.stackCount === this.getMaxHeldItemCount()) { - statHolder.value = Math.floor(statHolder.value * 1.05); - } - } - - return true; - } - - getScoreMultiplier(): number { - return 1.2; - } - - getMaxHeldItemCount(_pokemon?: Pokemon): number { - return 50; - } -} - -/** - * Modifier used for held items that Applies {@linkcode Stat} boost(s) - * using a multiplier. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class StatBoosterModifier extends PokemonHeldItemModifier { - /** The stats that the held item boosts */ - protected stats: Stat[]; - /** The multiplier used to increase the relevant stat(s) */ - protected multiplier: number; - - constructor(type: ModifierType, pokemonId: number, stats: Stat[], multiplier: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.stats = stats; - this.multiplier = multiplier; - } - - clone() { - return new StatBoosterModifier(this.type, this.pokemonId, this.stats, this.multiplier, this.stackCount); - } - - getArgs(): any[] { - return [...super.getArgs(), this.stats, this.multiplier]; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof StatBoosterModifier) { - const modifierInstance = modifier as StatBoosterModifier; - if (modifierInstance.multiplier === this.multiplier && modifierInstance.stats.length === this.stats.length) { - return modifierInstance.stats.every((e, i) => e === this.stats[i]); - } - } - - return false; - } - - /** - * Checks if the incoming stat is listed in {@linkcode stats} - * @param _pokemon the {@linkcode Pokemon} that holds the item - * @param _stat the {@linkcode Stat} to be boosted - * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat could be boosted, false otherwise - */ - override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { - return super.shouldApply(pokemon, stat, statValue) && this.stats.includes(stat); - } - - /** - * Boosts the incoming stat by a {@linkcode multiplier} if the stat is listed - * in {@linkcode stats}. - * @param _pokemon the {@linkcode Pokemon} that holds the item - * @param _stat the {@linkcode Stat} to be boosted - * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat boost applies successfully, false otherwise - * @see shouldApply - */ - override apply(_pokemon: Pokemon, _stat: Stat, statValue: NumberHolder): boolean { - statValue.value *= this.multiplier; - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Modifier used for held items, specifically Eviolite, that apply - * {@linkcode Stat} boost(s) using a multiplier if the holder can evolve. - * @extends StatBoosterModifier - * @see {@linkcode apply} - */ -export class EvolutionStatBoosterModifier extends StatBoosterModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof EvolutionStatBoosterModifier; - } - - /** - * Checks if the stat boosts can apply and if the holder is not currently - * Gigantamax'd. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param stat {@linkcode Stat} The {@linkcode Stat} to be boosted - * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat boosts can be applied, false otherwise - */ - override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { - return super.shouldApply(pokemon, stat, statValue) && !pokemon.isMax(); - } - - /** - * Boosts the incoming stat value by a {@linkcode EvolutionStatBoosterModifier.multiplier} if the holder - * can evolve. Note that, if the holder is a fusion, they will receive - * only half of the boost if either of the fused members are fully - * evolved. However, if they are both unevolved, the full boost - * will apply. - * @param pokemon {@linkcode Pokemon} that holds the item - * @param _stat {@linkcode Stat} The {@linkcode Stat} to be boosted - * @param statValue{@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat boost applies successfully, false otherwise - * @see shouldApply - */ - override apply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { - const isUnevolved = pokemon.getSpeciesForm(true).speciesId in pokemonEvolutions; - - if (pokemon.isFusion() && pokemon.getFusionSpeciesForm(true).speciesId in pokemonEvolutions !== isUnevolved) { - // Half boost applied if pokemon is fused and either part of fusion is fully evolved - statValue.value *= 1 + (this.multiplier - 1) / 2; - return true; - } - if (isUnevolved) { - // Full boost applied if holder is unfused and unevolved or, if fused, both parts of fusion are unevolved - return super.apply(pokemon, stat, statValue); - } - - return false; - } -} - -/** - * Modifier used for held items that Applies {@linkcode Stat} boost(s) using a - * multiplier if the holder is of a specific {@linkcode SpeciesId}. - * @extends StatBoosterModifier - * @see {@linkcode apply} - */ -export class SpeciesStatBoosterModifier extends StatBoosterModifier { - /** The species that the held item's stat boost(s) apply to */ - private species: SpeciesId[]; - - constructor( - type: ModifierType, - pokemonId: number, - stats: Stat[], - multiplier: number, - species: SpeciesId[], - stackCount?: number, - ) { - super(type, pokemonId, stats, multiplier, stackCount); - - this.species = species; - } - - clone() { - return new SpeciesStatBoosterModifier( - this.type, - this.pokemonId, - this.stats, - this.multiplier, - this.species, - this.stackCount, - ); - } - - getArgs(): any[] { - return [...super.getArgs(), this.species]; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof SpeciesStatBoosterModifier) { - const modifierInstance = modifier as SpeciesStatBoosterModifier; - if (modifierInstance.species.length === this.species.length) { - return super.matchType(modifier) && modifierInstance.species.every((e, i) => e === this.species[i]); - } - } - - return false; - } - - /** - * Checks if the incoming stat is listed in {@linkcode stats} and if the holder's {@linkcode SpeciesId} - * (or its fused species) is listed in {@linkcode species}. - * @param pokemon {@linkcode Pokemon} that holds the item - * @param stat {@linkcode Stat} being checked at the time - * @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat - * @returns `true` if the stat could be boosted, false otherwise - */ - override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean { - return ( - super.shouldApply(pokemon, stat, statValue) && - (this.species.includes(pokemon.getSpeciesForm(true).speciesId) || - (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId))) - ); - } - - /** - * Checks if either parameter is included in the corresponding lists - * @param speciesId {@linkcode SpeciesId} being checked - * @param stat {@linkcode Stat} being checked - * @returns `true` if both parameters are in {@linkcode species} and {@linkcode stats} respectively, false otherwise - */ - contains(speciesId: SpeciesId, stat: Stat): boolean { - return this.species.includes(speciesId) && this.stats.includes(stat); - } -} - -/** - * Modifier used for held items that apply critical-hit stage boost(s). - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class CritBoosterModifier extends PokemonHeldItemModifier { - /** The amount of stages by which the held item increases the current critical-hit stage value */ - protected stageIncrement: number; - - constructor(type: ModifierType, pokemonId: number, stageIncrement: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.stageIncrement = stageIncrement; - } - - clone() { - return new CritBoosterModifier(this.type, this.pokemonId, this.stageIncrement, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.stageIncrement); - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof CritBoosterModifier) { - return (modifier as CritBoosterModifier).stageIncrement === this.stageIncrement; - } - - return false; - } - - /** - * Increases the current critical-hit stage value by {@linkcode stageIncrement}. - * @param _pokemon {@linkcode Pokemon} N/A - * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns always `true` - */ - override apply(_pokemon: Pokemon, critStage: NumberHolder): boolean { - critStage.value += this.stageIncrement; - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Modifier used for held items that apply critical-hit stage boost(s) - * if the holder is of a specific {@linkcode SpeciesId}. - * @extends CritBoosterModifier - * @see {@linkcode shouldApply} - */ -export class SpeciesCritBoosterModifier extends CritBoosterModifier { - /** The species that the held item's critical-hit stage boost applies to */ - private species: SpeciesId[]; - - constructor( - type: ModifierType, - pokemonId: number, - stageIncrement: number, - species: SpeciesId[], - stackCount?: number, - ) { - super(type, pokemonId, stageIncrement, stackCount); - - this.species = species; - } - - clone() { - return new SpeciesCritBoosterModifier( - this.type, - this.pokemonId, - this.stageIncrement, - this.species, - this.stackCount, - ); - } - - getArgs(): any[] { - return [...super.getArgs(), this.species]; - } - - matchType(modifier: Modifier): boolean { - return modifier instanceof SpeciesCritBoosterModifier; - } - - /** - * Checks if the holder's {@linkcode SpeciesId} (or its fused species) is listed - * in {@linkcode species}. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns `true` if the critical-hit level can be incremented, false otherwise - */ - override shouldApply(pokemon: Pokemon, critStage: NumberHolder): boolean { - return ( - super.shouldApply(pokemon, critStage) && - (this.species.includes(pokemon.getSpeciesForm(true).speciesId) || - (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId))) - ); - } -} - -/** - * Applies Specific Type item boosts (e.g., Magnet) - */ -export class AttackTypeBoosterModifier extends PokemonHeldItemModifier { - public moveType: PokemonType; - private boostMultiplier: number; - - constructor(type: ModifierType, pokemonId: number, moveType: PokemonType, boostPercent: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.moveType = moveType; - this.boostMultiplier = boostPercent * 0.01; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof AttackTypeBoosterModifier) { - const attackTypeBoosterModifier = modifier as AttackTypeBoosterModifier; - return ( - attackTypeBoosterModifier.moveType === this.moveType && - attackTypeBoosterModifier.boostMultiplier === this.boostMultiplier - ); - } - - return false; - } - - clone() { - return new AttackTypeBoosterModifier( - this.type, - this.pokemonId, - this.moveType, - this.boostMultiplier * 100, - this.stackCount, - ); - } - - getArgs(): any[] { - return super.getArgs().concat([this.moveType, this.boostMultiplier * 100]); - } - - /** - * Checks if {@linkcode AttackTypeBoosterModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the held item - * @param moveType the {@linkcode PokemonType} of the move being used - * @param movePower the {@linkcode NumberHolder} that holds the power of the move - * @returns `true` if boosts should be applied to the move. - */ - override shouldApply(pokemon?: Pokemon, moveType?: PokemonType, movePower?: NumberHolder): boolean { - return ( - super.shouldApply(pokemon, moveType, movePower) && - typeof moveType === "number" && - movePower instanceof NumberHolder && - this.moveType === moveType - ); - } - - /** - * Applies {@linkcode AttackTypeBoosterModifier} - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param moveType {@linkcode PokemonType} of the move being used - * @param movePower {@linkcode NumberHolder} that holds the power of the move - * @returns `true` if boosts have been applied to the move. - */ - override apply(_pokemon: Pokemon, moveType: PokemonType, movePower: NumberHolder): boolean { - if (moveType === this.moveType && movePower.value >= 1) { - (movePower as NumberHolder).value = Math.floor( - (movePower as NumberHolder).value * (1 + this.getStackCount() * this.boostMultiplier), - ); - return true; - } - - return false; - } - - getScoreMultiplier(): number { - return 1.2; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 99; - } -} - -export class SurviveDamageModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof SurviveDamageModifier; - } - - clone() { - return new SurviveDamageModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if the {@linkcode SurviveDamageModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage - * @returns `true` if the {@linkcode SurviveDamageModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, surviveDamage?: BooleanHolder): boolean { - return super.shouldApply(pokemon, surviveDamage) && !!surviveDamage; - } - - /** - * Applies {@linkcode SurviveDamageModifier} - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage - * @returns `true` if the survive damage has been applied - */ - override apply(pokemon: Pokemon, surviveDamage: BooleanHolder): boolean { - if (!surviveDamage.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - surviveDamage.value = true; - - globalScene.phaseManager.queueMessage( - i18next.t("modifier:surviveDamageApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - ); - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 5; - } -} - -export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof BypassSpeedChanceModifier; - } - - clone() { - return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if {@linkcode BypassSpeedChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { - return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; - } - - /** - * Applies {@linkcode BypassSpeedChanceModifier} - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied - */ - override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean { - if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - doBypassSpeed.value = true; - const isCommandFight = - globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; - const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW"; - - if (isCommandFight && hasQuickClaw) { - globalScene.phaseManager.queueMessage( - i18next.t("modifier:bypassSpeedChanceApply", { - pokemonName: getPokemonNameWithAffix(pokemon), - itemName: i18next.t("modifierType:ModifierType.QUICK_CLAW.name"), - }), - ); - } - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -/** - * Class for Pokemon held items like King's Rock - * Because King's Rock can be stacked in PokeRogue, unlike mainline, it does not receive a boost from AbilityId.SERENE_GRACE - */ -export class FlinchChanceModifier extends PokemonHeldItemModifier { - private chance: number; - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.chance = 10; - } - - matchType(modifier: Modifier) { - return modifier instanceof FlinchChanceModifier; - } - - clone() { - return new FlinchChanceModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if {@linkcode FlinchChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param flinched {@linkcode BooleanHolder} that is `true` if the pokemon flinched - * @returns `true` if {@linkcode FlinchChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, flinched?: BooleanHolder): boolean { - return super.shouldApply(pokemon, flinched) && !!flinched; - } - - /** - * Applies {@linkcode FlinchChanceModifier} to randomly flinch targets hit. - * @param pokemon - The {@linkcode Pokemon} that holds the item - * @param flinched - A {@linkcode BooleanHolder} holding whether the pokemon has flinched - * @returns `true` if {@linkcode FlinchChanceModifier} was applied successfully - */ - override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean { - // The check for pokemon.summonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch - // TODO: Since summonData is always defined now, we can probably remove this - if (pokemon.summonData && !flinched.value && pokemon.randBattleSeedInt(100) < this.getStackCount() * this.chance) { - flinched.value = true; - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -export class TurnHealModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof TurnHealModifier; - } - - clone() { - return new TurnHealModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode TurnHealModifier} - * @param pokemon The {@linkcode Pokemon} that holds the item - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(pokemon: Pokemon): boolean { - if (!pokemon.isFullHp()) { - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - pokemon.getBattlerIndex(), - toDmgValue(pokemon.getMaxHp() / 16) * this.stackCount, - i18next.t("modifier:turnHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - true, - ); - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 4; - } -} - -/** - * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a - * set {@linkcode StatusEffect} at the end of a turn. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class TurnStatusEffectModifier extends PokemonHeldItemModifier { - /** The status effect to be applied by the held item */ - private effect: StatusEffect; - - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - switch (type.id) { - case "TOXIC_ORB": - this.effect = StatusEffect.TOXIC; - break; - case "FLAME_ORB": - this.effect = StatusEffect.BURN; - break; - } - } - - /** - * Checks if {@linkcode modifier} is an instance of this class, - * intentionally ignoring potentially different {@linkcode effect}s - * to prevent held item stockpiling since the item obtained first - * would be the only item able to {@linkcode apply} successfully. - * @override - * @param modifier {@linkcode Modifier} being type tested - * @return `true` if {@linkcode modifier} is an instance of - * TurnStatusEffectModifier, false otherwise - */ - matchType(modifier: Modifier): boolean { - return modifier instanceof TurnStatusEffectModifier; - } - - clone() { - return new TurnStatusEffectModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Tries to inflicts the holder with the associated {@linkcode StatusEffect}. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @returns `true` if the status effect was applied successfully - */ - override apply(pokemon: Pokemon): boolean { - return pokemon.trySetStatus(this.effect, true, undefined, undefined, this.type.name); - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } - - getStatusEffect(): StatusEffect { - return this.effect; - } -} - -export class HitHealModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof HitHealModifier; - } - - clone() { - return new HitHealModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode HitHealModifier} - * @param pokemon The {@linkcode Pokemon} that holds the item - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(pokemon: Pokemon): boolean { - if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) { - // TODO: this shouldn't be undefined AFAIK - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - pokemon.getBattlerIndex(), - toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, - i18next.t("modifier:hitHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - true, - ); - } - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 4; - } -} - -export class LevelIncrementBoosterModifier extends PersistentModifier { - match(modifier: Modifier) { - return modifier instanceof LevelIncrementBoosterModifier; - } - - clone() { - return new LevelIncrementBoosterModifier(this.type, this.stackCount); - } - - /** - * Checks if {@linkcode LevelIncrementBoosterModifier} should be applied - * @param count {@linkcode NumberHolder} holding the level increment count - * @returns `true` if {@linkcode LevelIncrementBoosterModifier} should be applied - */ - override shouldApply(count: NumberHolder): boolean { - return !!count; - } - - /** - * Applies {@linkcode LevelIncrementBoosterModifier} - * @param count {@linkcode NumberHolder} holding the level increment count - * @returns always `true` - */ - override apply(count: NumberHolder): boolean { - count.value += this.getStackCount(); - - return true; - } - - getMaxStackCount(_forThreshold?: boolean): number { - return 99; - } -} - -export class BerryModifier extends PokemonHeldItemModifier { - public berryType: BerryType; - public consumed: boolean; - - constructor(type: ModifierType, pokemonId: number, berryType: BerryType, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.berryType = berryType; - this.consumed = false; - } - - matchType(modifier: Modifier) { - return modifier instanceof BerryModifier && (modifier as BerryModifier).berryType === this.berryType; - } - - clone() { - return new BerryModifier(this.type, this.pokemonId, this.berryType, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.berryType); - } - - /** - * Checks if {@linkcode BerryModifier} should be applied - * @param pokemon The {@linkcode Pokemon} that holds the berry - * @returns `true` if {@linkcode BerryModifier} should be applied - */ - override shouldApply(pokemon: Pokemon): boolean { - return !this.consumed && super.shouldApply(pokemon) && getBerryPredicate(this.berryType)(pokemon); - } - - /** - * Applies {@linkcode BerryModifier} - * @param pokemon The {@linkcode Pokemon} that holds the berry - * @returns always `true` - */ - override apply(pokemon: Pokemon): boolean { - const preserve = new BooleanHolder(false); - globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); - this.consumed = !preserve.value; - - // munch the berry and trigger unburden-like effects - getBerryEffectFunc(this.berryType)(pokemon); - applyAbAttrs("PostItemLostAbAttr", { pokemon }); - - // Update berry eaten trackers for Belch, Harvest, Cud Chew, etc. - // Don't recover it if we proc berry pouch (no item duplication) - pokemon.recordEatenBerry(this.berryType, this.consumed); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - if ([BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(this.berryType)) { - return 2; - } - return 3; - } -} - -export class PreserveBerryModifier extends PersistentModifier { - match(modifier: Modifier) { - return modifier instanceof PreserveBerryModifier; - } - - clone() { - return new PreserveBerryModifier(this.type, this.stackCount); - } - - /** - * Checks if all prequired conditions are met to apply {@linkcode PreserveBerryModifier} - * @param pokemon {@linkcode Pokemon} that holds the berry - * @param doPreserve {@linkcode BooleanHolder} that is `true` if the berry should be preserved - * @returns `true` if {@linkcode PreserveBerryModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doPreserve?: BooleanHolder): boolean { - return !!pokemon && !!doPreserve; - } - - /** - * Applies {@linkcode PreserveBerryModifier} - * @param pokemon The {@linkcode Pokemon} that holds the berry - * @param doPreserve {@linkcode BooleanHolder} that is `true` if the berry should be preserved - * @returns always `true` - */ - override apply(pokemon: Pokemon, doPreserve: BooleanHolder): boolean { - doPreserve.value ||= pokemon.randBattleSeedInt(10) < this.getStackCount() * 3; - - return true; - } - - getMaxStackCount(): number { - return 3; - } -} - -export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof PokemonInstantReviveModifier; - } - - clone() { - return new PokemonInstantReviveModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode PokemonInstantReviveModifier} - * @param pokemon The {@linkcode Pokemon} that holds the item - * @returns always `true` - */ - override apply(pokemon: Pokemon): boolean { - // Restore the Pokemon to half HP - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - pokemon.getBattlerIndex(), - toDmgValue(pokemon.getMaxHp() / 2), - i18next.t("modifier:pokemonInstantReviveApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - false, - false, - true, - ); - - // Remove the Pokemon's FAINT status - pokemon.resetStatus(true, false, true, false); - - // Reapply Commander on the Pokemon's side of the field, if applicable - const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - for (const p of field) { - applyAbAttrs("CommanderAbAttr", { pokemon: p }); - } - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Modifier used for held items, namely White Herb, that restore adverse stat - * stages in battle. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof ResetNegativeStatStageModifier; - } - - clone() { - return new ResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Goes through the holder's stat stages and, if any are negative, resets that - * stat stage back to 0. - * @param pokemon {@linkcode Pokemon} that holds the item - * @returns `true` if any stat stages were reset, false otherwise - */ - override apply(pokemon: Pokemon): boolean { - let statRestored = false; - - for (const s of BATTLE_STATS) { - if (pokemon.getStatStage(s) < 0) { - pokemon.setStatStage(s, 0); - statRestored = true; - } - } - - if (statRestored) { - globalScene.phaseManager.queueMessage( - i18next.t("modifier:resetNegativeStatStageApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - ); - } - return statRestored; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 2; - } -} - -/** - * Modifier used for held items, namely Mystical Rock, that extend the - * duration of weather and terrain effects. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class FieldEffectModifier extends PokemonHeldItemModifier { - /** - * Provides two more turns per stack to any weather or terrain effect caused - * by the holder. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param fieldDuration {@linkcode NumberHolder} that stores the current field effect duration - * @returns `true` if the field effect extension was applied successfully - */ - override apply(_pokemon: Pokemon, fieldDuration: NumberHolder): boolean { - fieldDuration.value += 2 * this.stackCount; - return true; - } - - override matchType(modifier: Modifier): boolean { - return modifier instanceof FieldEffectModifier; - } - - override clone(): FieldEffectModifier { - return new FieldEffectModifier(this.type, this.pokemonId, this.stackCount); - } - - override getMaxHeldItemCount(_pokemon?: Pokemon): number { - return 2; - } -} - -export abstract class ConsumablePokemonModifier extends ConsumableModifier { - public pokemonId: number; - - constructor(type: ModifierType, pokemonId: number) { - super(type); - - this.pokemonId = pokemonId; - } - - /** - * Checks if {@linkcode ConsumablePokemonModifier} should be applied - * @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item - * @param _args N/A - * @returns `true` if {@linkcode ConsumablePokemonModifier} should be applied - */ - override shouldApply(playerPokemon?: PlayerPokemon, ..._args: unknown[]): boolean { - return !!playerPokemon && (this.pokemonId === -1 || playerPokemon.id === this.pokemonId); - } - - /** - * Applies {@linkcode ConsumablePokemonModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item - * @param args Additional arguments passed to {@linkcode ConsumablePokemonModifier.apply} - */ - abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean; - - getPokemon() { - return globalScene.getPlayerParty().find(p => p.id === this.pokemonId); - } -} - -export class TerrastalizeModifier extends ConsumablePokemonModifier { - public declare type: TerastallizeModifierType; - public teraType: PokemonType; - - constructor(type: TerastallizeModifierType, pokemonId: number, teraType: PokemonType) { - super(type, pokemonId); - - this.teraType = teraType; - } - - /** - * Checks if {@linkcode TerrastalizeModifier} should be applied - * @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item - * @returns `true` if the {@linkcode TerrastalizeModifier} should be applied - */ - override shouldApply(playerPokemon?: PlayerPokemon): boolean { - return ( - super.shouldApply(playerPokemon) && - [playerPokemon?.species.speciesId, playerPokemon?.fusionSpecies?.speciesId].filter( - s => s === SpeciesId.TERAPAGOS || s === SpeciesId.OGERPON || s === SpeciesId.SHEDINJA, - ).length === 0 - ); - } - - /** - * Applies {@linkcode TerrastalizeModifier} - * @param pokemon The {@linkcode PlayerPokemon} that consumes the item - * @returns `true` if hp was restored - */ - override apply(pokemon: Pokemon): boolean { - pokemon.teraType = this.teraType; - return true; - } -} - -export class PokemonHpRestoreModifier extends ConsumablePokemonModifier { - private restorePoints: number; - private restorePercent: number; - private healStatus: boolean; - public fainted: boolean; - - constructor( - type: ModifierType, - pokemonId: number, - restorePoints: number, - restorePercent: number, - healStatus: boolean, - fainted?: boolean, - ) { - super(type, pokemonId); - - this.restorePoints = restorePoints; - this.restorePercent = restorePercent; - this.healStatus = healStatus; - this.fainted = !!fainted; - } - - /** - * Checks if {@linkcode PokemonHpRestoreModifier} should be applied - * @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item - * @param multiplier The multiplier of the hp restore - * @returns `true` if the {@linkcode PokemonHpRestoreModifier} should be applied - */ - override shouldApply(playerPokemon?: PlayerPokemon, multiplier?: number): boolean { - return ( - super.shouldApply(playerPokemon) && - (this.fainted || (!isNullOrUndefined(multiplier) && typeof multiplier === "number")) - ); - } - - /** - * Applies {@linkcode PokemonHpRestoreModifier} - * @param pokemon The {@linkcode PlayerPokemon} that consumes the item - * @param multiplier The multiplier of the hp restore - * @returns `true` if hp was restored - */ - override apply(pokemon: Pokemon, multiplier: number): boolean { - if (!pokemon.hp === this.fainted) { - let restorePoints = this.restorePoints; - if (!this.fainted) { - restorePoints = Math.floor(restorePoints * multiplier); - } - if (this.fainted || this.healStatus) { - pokemon.resetStatus(true, true, false, false); - } - pokemon.hp = Math.min( - pokemon.hp + - Math.max(Math.ceil(Math.max(Math.floor(this.restorePercent * 0.01 * pokemon.getMaxHp()), restorePoints)), 1), - pokemon.getMaxHp(), - ); - return true; - } - return false; - } -} - -export class PokemonStatusHealModifier extends ConsumablePokemonModifier { - /** - * Applies {@linkcode PokemonStatusHealModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that gets healed from the status - * @returns always `true` - */ - override apply(playerPokemon: PlayerPokemon): boolean { - playerPokemon.resetStatus(true, true, false, false); - return true; - } -} - -export abstract class ConsumablePokemonMoveModifier extends ConsumablePokemonModifier { - public moveIndex: number; - - constructor(type: ModifierType, pokemonId: number, moveIndex: number) { - super(type, pokemonId); - - this.moveIndex = moveIndex; - } -} - -export class PokemonPpRestoreModifier extends ConsumablePokemonMoveModifier { - private restorePoints: number; - - constructor(type: ModifierType, pokemonId: number, moveIndex: number, restorePoints: number) { - super(type, pokemonId, moveIndex); - - this.restorePoints = restorePoints; - } - - /** - * Applies {@linkcode PokemonPpRestoreModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that should get move pp restored - * @returns always `true` - */ - override apply(playerPokemon: PlayerPokemon): boolean { - const move = playerPokemon.getMoveset()[this.moveIndex]; - - if (move) { - move.ppUsed = this.restorePoints > -1 ? Math.max(move.ppUsed - this.restorePoints, 0) : 0; - } - - return true; - } -} - -export class PokemonAllMovePpRestoreModifier extends ConsumablePokemonModifier { - private restorePoints: number; - - constructor(type: ModifierType, pokemonId: number, restorePoints: number) { - super(type, pokemonId); - - this.restorePoints = restorePoints; - } - - /** - * Applies {@linkcode PokemonAllMovePpRestoreModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that should get all move pp restored - * @returns always `true` - */ - override apply(playerPokemon: PlayerPokemon): boolean { - for (const move of playerPokemon.getMoveset()) { - if (move) { - move.ppUsed = this.restorePoints > -1 ? Math.max(move.ppUsed - this.restorePoints, 0) : 0; - } - } - - return true; - } -} - -export class PokemonPpUpModifier extends ConsumablePokemonMoveModifier { - private upPoints: number; - - constructor(type: ModifierType, pokemonId: number, moveIndex: number, upPoints: number) { - super(type, pokemonId, moveIndex); - - this.upPoints = upPoints; - } - - /** - * Applies {@linkcode PokemonPpUpModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that gets a pp up on move-slot {@linkcode moveIndex} - * @returns - */ - override apply(playerPokemon: PlayerPokemon): boolean { - const move = playerPokemon.getMoveset()[this.moveIndex]; - - if (move && !move.maxPpOverride) { - move.ppUp = Math.min(move.ppUp + this.upPoints, 3); - } - - return true; - } -} - -export class PokemonNatureChangeModifier extends ConsumablePokemonModifier { - public nature: Nature; - - constructor(type: ModifierType, pokemonId: number, nature: Nature) { - super(type, pokemonId); - - this.nature = nature; - } - - /** - * Applies {@linkcode PokemonNatureChangeModifier} - * @param playerPokemon {@linkcode PlayerPokemon} to apply the {@linkcode Nature} change to - * @returns - */ - override apply(playerPokemon: PlayerPokemon): boolean { - playerPokemon.setCustomNature(this.nature); - globalScene.gameData.unlockSpeciesNature(playerPokemon.species, this.nature); - - return true; - } -} - -export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier { - /** - * Applies {@linkcode PokemonLevelIncrementModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that should get levels incremented - * @param levelCount The amount of levels to increment - * @returns always `true` - */ - override apply(playerPokemon: PlayerPokemon, levelCount: NumberHolder = new NumberHolder(1)): boolean { - globalScene.applyModifiers(LevelIncrementBoosterModifier, true, levelCount); - - playerPokemon.level += levelCount.value; - if (playerPokemon.level <= globalScene.getMaxExpLevel(true)) { - playerPokemon.exp = getLevelTotalExp(playerPokemon.level, playerPokemon.species.growthRate); - playerPokemon.levelExp = 0; - } - - playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY); - - globalScene.phaseManager.unshiftNew( - "LevelUpPhase", - globalScene.getPlayerParty().indexOf(playerPokemon), - playerPokemon.level - levelCount.value, - playerPokemon.level, - ); - - return true; - } -} - -export class TmModifier extends ConsumablePokemonModifier { - public declare type: TmModifierType; - - /** - * Applies {@linkcode TmModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that should learn the TM - * @returns always `true` - */ - override apply(playerPokemon: PlayerPokemon): boolean { - globalScene.phaseManager.unshiftNew( - "LearnMovePhase", - globalScene.getPlayerParty().indexOf(playerPokemon), - this.type.moveId, - LearnMoveType.TM, - ); - - return true; - } -} - -export class RememberMoveModifier extends ConsumablePokemonModifier { - public levelMoveIndex: number; - - constructor(type: ModifierType, pokemonId: number, levelMoveIndex: number) { - super(type, pokemonId); - - this.levelMoveIndex = levelMoveIndex; - } - - /** - * Applies {@linkcode RememberMoveModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that should remember the move - * @returns always `true` - */ - override apply(playerPokemon: PlayerPokemon, cost?: number): boolean { - globalScene.phaseManager.unshiftNew( - "LearnMovePhase", - globalScene.getPlayerParty().indexOf(playerPokemon), - playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], - LearnMoveType.MEMORY, - cost, - ); - - return true; - } -} - -export class EvolutionItemModifier extends ConsumablePokemonModifier { - public declare type: EvolutionItemModifierType; - /** - * Applies {@linkcode EvolutionItemModifier} - * @param playerPokemon The {@linkcode PlayerPokemon} that should evolve via item - * @returns `true` if the evolution was successful - */ - override apply(playerPokemon: PlayerPokemon): boolean { - let matchingEvolution = pokemonEvolutions.hasOwnProperty(playerPokemon.species.speciesId) - ? pokemonEvolutions[playerPokemon.species.speciesId].find( - e => e.evoItem === this.type.evolutionItem && e.validate(playerPokemon, false, e.item!), - ) - : null; - - if (!matchingEvolution && playerPokemon.isFusion()) { - matchingEvolution = pokemonEvolutions[playerPokemon.fusionSpecies!.speciesId].find( - e => e.evoItem === this.type.evolutionItem && e.validate(playerPokemon, true, e.item!), - ); - if (matchingEvolution) { - matchingEvolution = new FusionSpeciesFormEvolution(playerPokemon.species.speciesId, matchingEvolution); - } - } - - if (matchingEvolution) { - globalScene.phaseManager.unshiftNew("EvolutionPhase", playerPokemon, matchingEvolution, playerPokemon.level - 1); - return true; - } - - return false; - } -} - -export class FusePokemonModifier extends ConsumablePokemonModifier { - public fusePokemonId: number; - - constructor(type: ModifierType, pokemonId: number, fusePokemonId: number) { - super(type, pokemonId); - - this.fusePokemonId = fusePokemonId; - } - - /** - * Checks if {@linkcode FusePokemonModifier} should be applied - * @param playerPokemon {@linkcode PlayerPokemon} that should be fused - * @param playerPokemon2 {@linkcode PlayerPokemon} that should be fused with {@linkcode playerPokemon} - * @returns `true` if {@linkcode FusePokemonModifier} should be applied - */ - override shouldApply(playerPokemon?: PlayerPokemon, playerPokemon2?: PlayerPokemon): boolean { - return ( - super.shouldApply(playerPokemon, playerPokemon2) && !!playerPokemon2 && this.fusePokemonId === playerPokemon2.id - ); - } - - /** - * Applies {@linkcode FusePokemonModifier} - * @param playerPokemon {@linkcode PlayerPokemon} that should be fused - * @param playerPokemon2 {@linkcode PlayerPokemon} that should be fused with {@linkcode playerPokemon} - * @returns always Promise - */ - override apply(playerPokemon: PlayerPokemon, playerPokemon2: PlayerPokemon): boolean { - playerPokemon.fuse(playerPokemon2); - return true; - } -} - -export class MultipleParticipantExpBonusModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof MultipleParticipantExpBonusModifier; - } - - /** - * Applies {@linkcode MultipleParticipantExpBonusModifier} - * @returns always `true` - */ - apply(): boolean { - return true; - } - - clone(): MultipleParticipantExpBonusModifier { - return new MultipleParticipantExpBonusModifier(this.type, this.stackCount); - } - - getMaxStackCount(): number { - return 5; - } -} - -export class HealingBoosterModifier extends PersistentModifier { - private multiplier: number; - - constructor(type: ModifierType, multiplier: number, stackCount?: number) { - super(type, stackCount); - - this.multiplier = multiplier; - } - - match(modifier: Modifier): boolean { - return modifier instanceof HealingBoosterModifier; - } - - clone(): HealingBoosterModifier { - return new HealingBoosterModifier(this.type, this.multiplier, this.stackCount); - } - - getArgs(): any[] { - return [this.multiplier]; - } - - /** - * Applies {@linkcode HealingBoosterModifier} - * @param healingMultiplier the multiplier to apply to the healing - * @returns always `true` - */ - override apply(healingMultiplier: NumberHolder): boolean { - healingMultiplier.value *= 1 + (this.multiplier - 1) * this.getStackCount(); - - return true; - } - - getMaxStackCount(): number { - return 5; - } -} - -export class ExpBoosterModifier extends PersistentModifier { - private boostMultiplier: number; - - constructor(type: ModifierType, boostPercent: number, stackCount?: number) { - super(type, stackCount); - - this.boostMultiplier = boostPercent * 0.01; - } - - match(modifier: Modifier): boolean { - if (modifier instanceof ExpBoosterModifier) { - const expModifier = modifier as ExpBoosterModifier; - return expModifier.boostMultiplier === this.boostMultiplier; - } - return false; - } - - clone(): ExpBoosterModifier { - return new ExpBoosterModifier(this.type, this.boostMultiplier * 100, this.stackCount); - } - - getArgs(): any[] { - return [this.boostMultiplier * 100]; - } - - /** - * Applies {@linkcode ExpBoosterModifier} - * @param boost {@linkcode NumberHolder} holding the boost value - * @returns always `true` - */ - override apply(boost: NumberHolder): boolean { - boost.value = Math.floor(boost.value * (1 + this.getStackCount() * this.boostMultiplier)); - - return true; - } - - getMaxStackCount(_forThreshold?: boolean): number { - return this.boostMultiplier < 1 ? (this.boostMultiplier < 0.6 ? 99 : 30) : 10; - } -} - -export class PokemonExpBoosterModifier extends PokemonHeldItemModifier { - public declare type: PokemonExpBoosterModifierType; - - private boostMultiplier: number; - - constructor(type: PokemonExpBoosterModifierType, pokemonId: number, boostPercent: number, stackCount?: number) { - super(type, pokemonId, stackCount); - this.boostMultiplier = boostPercent * 0.01; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof PokemonExpBoosterModifier) { - const pokemonExpModifier = modifier as PokemonExpBoosterModifier; - return pokemonExpModifier.boostMultiplier === this.boostMultiplier; - } - return false; - } - - clone(): PersistentModifier { - return new PokemonExpBoosterModifier(this.type, this.pokemonId, this.boostMultiplier * 100, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.boostMultiplier * 100); - } - - /** - * Checks if {@linkcode PokemonExpBoosterModifier} should be applied - * @param pokemon The {@linkcode Pokemon} to apply the exp boost to - * @param boost {@linkcode NumberHolder} holding the exp boost value - * @returns `true` if {@linkcode PokemonExpBoosterModifier} should be applied - */ - override shouldApply(pokemon: Pokemon, boost: NumberHolder): boolean { - return super.shouldApply(pokemon, boost) && !!boost; - } - - /** - * Applies {@linkcode PokemonExpBoosterModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the exp boost to - * @param boost {@linkcode NumberHolder} holding the exp boost value - * @returns always `true` - */ - override apply(_pokemon: Pokemon, boost: NumberHolder): boolean { - boost.value = Math.floor(boost.value * (1 + this.getStackCount() * this.boostMultiplier)); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 99; - } -} - -export class ExpShareModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof ExpShareModifier; - } - - clone(): ExpShareModifier { - return new ExpShareModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode ExpShareModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxStackCount(): number { - return 5; - } -} - -export class ExpBalanceModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof ExpBalanceModifier; - } - - clone(): ExpBalanceModifier { - return new ExpBalanceModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode ExpBalanceModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxStackCount(): number { - return 4; - } -} - -export class PokemonFriendshipBoosterModifier extends PokemonHeldItemModifier { - public declare 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 declare 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 declare 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 declare 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; - - constructor(type: ModifierType, moneyMultiplier: number) { - super(type); - - this.moneyMultiplier = moneyMultiplier; - } - - /** - * Applies {@linkcode MoneyRewardModifier} - * @returns always `true` - */ - override apply(): boolean { - const moneyAmount = new NumberHolder(globalScene.getWaveMoneyAmount(this.moneyMultiplier)); - - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); - - globalScene.addMoney(moneyAmount.value); - - globalScene.getPlayerParty().map(p => { - if (p.species?.speciesId === SpeciesId.GIMMIGHOUL || p.fusionSpecies?.speciesId === SpeciesId.GIMMIGHOUL) { - const factor = Math.min(Math.floor(this.moneyMultiplier), 3); - const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier( - p, - factor, - ) as EvoTrackerModifier; - globalScene.addModifier(modifier); - } - }); - - return true; - } -} - -export class MoneyMultiplierModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof MoneyMultiplierModifier; - } - - clone(): MoneyMultiplierModifier { - return new MoneyMultiplierModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode MoneyMultiplierModifier} - * @param multiplier {@linkcode NumberHolder} holding the money multiplier value - * @returns always `true` - */ - override apply(multiplier: NumberHolder): boolean { - multiplier.value += Math.floor(multiplier.value * 0.2 * this.getStackCount()); - - return true; - } - - getMaxStackCount(): number { - return 5; - } -} - -export class DamageMoneyRewardModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof DamageMoneyRewardModifier; - } - - clone(): DamageMoneyRewardModifier { - return new DamageMoneyRewardModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode DamageMoneyRewardModifier} - * @param pokemon The {@linkcode Pokemon} attacking - * @param multiplier {@linkcode NumberHolder} holding the multiplier value - * @returns always `true` - */ - override apply(_pokemon: Pokemon, multiplier: NumberHolder): boolean { - const moneyAmount = new NumberHolder(Math.floor(multiplier.value * (0.5 * this.getStackCount()))); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); - globalScene.addMoney(moneyAmount.value); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 5; - } -} - -export class MoneyInterestModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof MoneyInterestModifier; - } - - /** - * Applies {@linkcode MoneyInterestModifier} - * @returns always `true` - */ - override apply(): boolean { - const interestAmount = Math.floor(globalScene.money * 0.1 * this.getStackCount()); - globalScene.addMoney(interestAmount); - - const userLocale = navigator.language || "en-US"; - const formattedMoneyAmount = interestAmount.toLocaleString(userLocale); - const message = i18next.t("modifier:moneyInterestApply", { - moneyAmount: formattedMoneyAmount, - typeName: this.type.name, - }); - globalScene.phaseManager.queueMessage(message, undefined, true); - - return true; - } - - clone(): MoneyInterestModifier { - return new MoneyInterestModifier(this.type, this.stackCount); - } - - getMaxStackCount(): number { - return 5; - } -} - -export class HiddenAbilityRateBoosterModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof HiddenAbilityRateBoosterModifier; - } - - clone(): HiddenAbilityRateBoosterModifier { - return new HiddenAbilityRateBoosterModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode HiddenAbilityRateBoosterModifier} - * @param boost {@linkcode NumberHolder} holding the boost value - * @returns always `true` - */ - override apply(boost: NumberHolder): boolean { - boost.value *= Math.pow(2, -1 - this.getStackCount()); - - return true; - } - - getMaxStackCount(): number { - return 4; - } -} - -export class ShinyRateBoosterModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof ShinyRateBoosterModifier; - } - - clone(): ShinyRateBoosterModifier { - return new ShinyRateBoosterModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode ShinyRateBoosterModifier} - * @param boost {@linkcode NumberHolder} holding the boost value - * @returns always `true` - */ - override apply(boost: NumberHolder): boolean { - boost.value *= Math.pow(2, 1 + this.getStackCount()); - - return true; - } - - getMaxStackCount(): number { - return 4; - } -} - -export class CriticalCatchChanceBoosterModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof CriticalCatchChanceBoosterModifier; - } - - clone(): CriticalCatchChanceBoosterModifier { - return new CriticalCatchChanceBoosterModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode CriticalCatchChanceBoosterModifier} - * @param boost {@linkcode NumberHolder} holding the boost value - * @returns always `true` - */ - override apply(boost: NumberHolder): boolean { - // 1 stack: 2x - // 2 stack: 2.5x - // 3 stack: 3x - boost.value *= 1.5 + this.getStackCount() / 2; - - return true; - } - - getMaxStackCount(): number { - return 3; - } -} - -export class LockModifierTiersModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof LockModifierTiersModifier; - } - - /** - * Applies {@linkcode LockModifierTiersModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - clone(): LockModifierTiersModifier { - return new LockModifierTiersModifier(this.type, this.stackCount); - } - - getMaxStackCount(): number { - return 1; - } -} - -/** - * Black Sludge item - */ -export class HealShopCostModifier extends PersistentModifier { - public readonly shopMultiplier: number; - - constructor(type: ModifierType, shopMultiplier: number, stackCount?: number) { - super(type, stackCount); - - this.shopMultiplier = shopMultiplier ?? 2.5; - } - - match(modifier: Modifier): boolean { - return modifier instanceof HealShopCostModifier; - } - - clone(): HealShopCostModifier { - return new HealShopCostModifier(this.type, this.shopMultiplier, this.stackCount); - } - - /** - * Applies {@linkcode HealShopCostModifier} - * @param cost {@linkcode NumberHolder} holding the heal shop cost - * @returns always `true` - */ - apply(moneyCost: NumberHolder): boolean { - moneyCost.value = Math.floor(moneyCost.value * this.shopMultiplier); - - return true; - } - - getArgs(): any[] { - return super.getArgs().concat(this.shopMultiplier); - } - - getMaxStackCount(): number { - return 1; - } -} - -export class BoostBugSpawnModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof BoostBugSpawnModifier; - } - - clone(): BoostBugSpawnModifier { - return new BoostBugSpawnModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode BoostBugSpawnModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxStackCount(): number { - return 1; - } -} - -export class SwitchEffectTransferModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof SwitchEffectTransferModifier; - } - - clone(): SwitchEffectTransferModifier { - return new SwitchEffectTransferModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode SwitchEffectTransferModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Abstract class for held items that steal other Pokemon's items. - * @see {@linkcode TurnHeldItemTransferModifier} - * @see {@linkcode ContactHeldItemTransferChanceModifier} - */ -export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { - /** - * Determines the targets to transfer items from when this applies. - * @param pokemon the {@linkcode Pokemon} holding this item - * @param _args N/A - * @returns the opponents of the source {@linkcode Pokemon} - */ - getTargets(pokemon?: Pokemon, ..._args: unknown[]): Pokemon[] { - return pokemon?.getOpponents?.() ?? []; - } - - /** - * Steals an item, chosen randomly, from a set of target Pokemon. - * @param pokemon The {@linkcode Pokemon} holding this item - * @param target The {@linkcode Pokemon} to steal from (optional) - * @param _args N/A - * @returns `true` if an item was stolen; false otherwise. - */ - override apply(pokemon: Pokemon, target?: Pokemon, ..._args: unknown[]): boolean { - const opponents = this.getTargets(pokemon, target); - - if (!opponents.length) { - return false; - } - - const targetPokemon = opponents[pokemon.randBattleSeedInt(opponents.length)]; - - const transferredItemCount = this.getTransferredItemCount(); - if (!transferredItemCount) { - return false; - } - - const transferredModifierTypes: ModifierType[] = []; - const itemModifiers = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === targetPokemon.id && m.isTransferable, - targetPokemon.isPlayer(), - ) as PokemonHeldItemModifier[]; - - for (let i = 0; i < transferredItemCount; i++) { - if (!itemModifiers.length) { - break; - } - const randItemIndex = pokemon.randBattleSeedInt(itemModifiers.length); - const randItem = itemModifiers[randItemIndex]; - if (globalScene.tryTransferHeldItemModifier(randItem, pokemon, false)) { - transferredModifierTypes.push(randItem.type); - itemModifiers.splice(randItemIndex, 1); - } - } - - for (const mt of transferredModifierTypes) { - globalScene.phaseManager.queueMessage(this.getTransferMessage(pokemon, targetPokemon, mt)); - } - - return !!transferredModifierTypes.length; - } - - abstract getTransferredItemCount(): number; - - abstract getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierType): string; -} - -/** - * Modifier for held items that steal items from the enemy at the end of - * each turn. - * @see {@linkcode modifierTypes[MINI_BLACK_HOLE]} - */ -export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { - isTransferable = true; - - matchType(modifier: Modifier): boolean { - return modifier instanceof TurnHeldItemTransferModifier; - } - - clone(): TurnHeldItemTransferModifier { - return new TurnHeldItemTransferModifier(this.type, this.pokemonId, this.stackCount); - } - - getTransferredItemCount(): number { - return this.getStackCount(); - } - - getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierType): string { - return i18next.t("modifier:turnHeldItemTransferApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), - itemName: item.name, - pokemonName: pokemon.getNameToRender(), - typeName: this.type.name, - }); - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } - - setTransferrableFalse(): void { - this.isTransferable = false; - } -} - -/** - * Modifier for held items that add a chance to steal items from the target of a - * successful attack. - * @see {@linkcode modifierTypes[GRIP_CLAW]} - * @see {@linkcode HeldItemTransferModifier} - */ -export class ContactHeldItemTransferChanceModifier extends HeldItemTransferModifier { - public readonly chance: number; - - constructor(type: ModifierType, pokemonId: number, chancePercent: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.chance = chancePercent / 100; - } - - /** - * Determines the target to steal items from when this applies. - * @param _holderPokemon The {@linkcode Pokemon} holding this item - * @param targetPokemon The {@linkcode Pokemon} the holder is targeting with an attack - * @returns The target {@linkcode Pokemon} as array for further use in `apply` implementations - */ - override getTargets(_holderPokemon: Pokemon, targetPokemon: Pokemon): Pokemon[] { - return targetPokemon ? [targetPokemon] : []; - } - - matchType(modifier: Modifier): boolean { - return modifier instanceof ContactHeldItemTransferChanceModifier; - } - - clone(): ContactHeldItemTransferChanceModifier { - return new ContactHeldItemTransferChanceModifier(this.type, this.pokemonId, this.chance * 100, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.chance * 100); - } - - getTransferredItemCount(): number { - return randSeedFloat() <= this.chance * this.getStackCount() ? 1 : 0; - } - - getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierType): string { - return i18next.t("modifier:contactHeldItemTransferApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), - itemName: item.name, - pokemonName: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }); - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 5; - } -} - -export class IvScannerModifier extends PersistentModifier { - constructor(type: ModifierType, _stackCount?: number) { - super(type); - } - - match(modifier: Modifier): boolean { - return modifier instanceof IvScannerModifier; - } - - clone(): IvScannerModifier { - return new IvScannerModifier(this.type); - } - - /** - * Applies {@linkcode IvScannerModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; //Dude are you kidding me - } - - getMaxStackCount(): number { - return 1; - } -} - -export class ExtraModifierModifier extends PersistentModifier { - match(modifier: Modifier): boolean { - return modifier instanceof ExtraModifierModifier; - } - - clone(): ExtraModifierModifier { - return new ExtraModifierModifier(this.type, this.stackCount); - } - - /** - * Applies {@linkcode ExtraModifierModifier} - * @param count {NumberHolder} holding the count value - * @returns always `true` - */ - override apply(count: NumberHolder): boolean { - count.value += this.getStackCount(); - - return true; - } - - getMaxStackCount(): number { - return 3; - } -} - -/** - * Modifier used for timed boosts to the player's shop item rewards. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} - */ -export class TempExtraModifierModifier extends LapsingPersistentModifier { - /** - * Goes through existing modifiers for any that match Silver Pokeball, - * which will then add the max count of the new item to the existing count of the current item. - * If no existing Silver Pokeballs are found, will add a new one. - * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers - * @param _virtual N/A - * @returns true if the modifier was successfully added or applied, false otherwise - */ - add(modifiers: PersistentModifier[], _virtual: boolean): boolean { - for (const modifier of modifiers) { - if (this.match(modifier)) { - const modifierInstance = modifier as TempExtraModifierModifier; - const newBattleCount = this.getMaxBattles() + modifierInstance.getBattleCount(); - - modifierInstance.setNewBattleCount(newBattleCount); - globalScene.playSound("se/restore"); - return true; - } - } - - modifiers.push(this); - return true; - } - - clone() { - return new TempExtraModifierModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount); - } - - match(modifier: Modifier): boolean { - return modifier instanceof TempExtraModifierModifier; - } - - /** - * Increases the current rewards in the battle by the `stackCount`. - * @returns `true` if the shop reward number modifier applies successfully - * @param count {@linkcode NumberHolder} that holds the resulting shop item reward count - */ - apply(count: NumberHolder): boolean { - count.value += this.getStackCount(); - return true; - } -} - -export abstract class EnemyPersistentModifier extends PersistentModifier { - getMaxStackCount(): number { - return 5; - } -} - -abstract class EnemyDamageMultiplierModifier extends EnemyPersistentModifier { - protected damageMultiplier: number; - - constructor(type: ModifierType, damageMultiplier: number, stackCount?: number) { - super(type, stackCount); - - this.damageMultiplier = damageMultiplier; - } - - /** - * Applies {@linkcode EnemyDamageMultiplierModifier} - * @param multiplier {NumberHolder} holding the multiplier value - * @returns always `true` - */ - override apply(multiplier: NumberHolder): boolean { - multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageMultiplier, this.getStackCount())); - - return true; - } - - getMaxStackCount(): number { - return 99; - } -} - -export class EnemyDamageBoosterModifier extends EnemyDamageMultiplierModifier { - constructor(type: ModifierType, _boostPercent: number, stackCount?: number) { - //super(type, 1 + ((boostPercent || 10) * 0.01), stackCount); - super(type, 1.05, stackCount); // Hardcode multiplier temporarily - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyDamageBoosterModifier; - } - - clone(): EnemyDamageBoosterModifier { - return new EnemyDamageBoosterModifier(this.type, (this.damageMultiplier - 1) * 100, this.stackCount); - } - - getArgs(): any[] { - return [(this.damageMultiplier - 1) * 100]; - } - - getMaxStackCount(): number { - return 999; - } -} - -export class EnemyDamageReducerModifier extends EnemyDamageMultiplierModifier { - constructor(type: ModifierType, _reductionPercent: number, stackCount?: number) { - //super(type, 1 - ((reductionPercent || 5) * 0.01), stackCount); - super(type, 0.975, stackCount); // Hardcode multiplier temporarily - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyDamageReducerModifier; - } - - clone(): EnemyDamageReducerModifier { - return new EnemyDamageReducerModifier(this.type, (1 - this.damageMultiplier) * 100, this.stackCount); - } - - getArgs(): any[] { - return [(1 - this.damageMultiplier) * 100]; - } - - getMaxStackCount(): number { - return globalScene.currentBattle.waveIndex < 2000 ? super.getMaxStackCount() : 999; - } -} - -export class EnemyTurnHealModifier extends EnemyPersistentModifier { - public healPercent: number; - - constructor(type: ModifierType, _healPercent: number, stackCount?: number) { - super(type, stackCount); - - // Hardcode temporarily - this.healPercent = 2; - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyTurnHealModifier; - } - - clone(): EnemyTurnHealModifier { - return new EnemyTurnHealModifier(this.type, this.healPercent, this.stackCount); - } - - getArgs(): any[] { - return [this.healPercent]; - } - - /** - * Applies {@linkcode EnemyTurnHealModifier} - * @param enemyPokemon The {@linkcode Pokemon} to heal - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(enemyPokemon: Pokemon): boolean { - if (!enemyPokemon.isFullHp()) { - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - enemyPokemon.getBattlerIndex(), - Math.max(Math.floor(enemyPokemon.getMaxHp() / (100 / this.healPercent)) * this.stackCount, 1), - i18next.t("modifier:enemyTurnHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), - }), - true, - false, - false, - false, - true, - ); - return true; - } - - return false; - } - - getMaxStackCount(): number { - return 10; - } -} - -export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifier { - public effect: StatusEffect; - public chance: number; - - constructor(type: ModifierType, effect: StatusEffect, _chancePercent: number, stackCount?: number) { - super(type, stackCount); - - this.effect = effect; - // Hardcode temporarily - this.chance = 0.025 * (this.effect === StatusEffect.BURN || this.effect === StatusEffect.POISON ? 2 : 1); - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyAttackStatusEffectChanceModifier && modifier.effect === this.effect; - } - - clone(): EnemyAttackStatusEffectChanceModifier { - return new EnemyAttackStatusEffectChanceModifier(this.type, this.effect, this.chance * 100, this.stackCount); - } - - getArgs(): any[] { - return [this.effect, this.chance * 100]; - } - - /** - * Applies {@linkcode EnemyAttackStatusEffectChanceModifier} - * @param enemyPokemon {@linkcode Pokemon} to apply the status effect to - * @returns `true` if the {@linkcode Pokemon} was affected - */ - override apply(enemyPokemon: Pokemon): boolean { - if (randSeedFloat() <= this.chance * this.getStackCount()) { - return enemyPokemon.trySetStatus(this.effect, true); - } - - return false; - } - - getMaxStackCount(): number { - return 10; - } -} - -export class EnemyStatusEffectHealChanceModifier extends EnemyPersistentModifier { - public chance: number; - - constructor(type: ModifierType, _chancePercent: number, stackCount?: number) { - super(type, stackCount); - - //Hardcode temporarily - this.chance = 0.025; - } - - match(modifier: Modifier): boolean { - return modifier instanceof EnemyStatusEffectHealChanceModifier; - } - - clone(): EnemyStatusEffectHealChanceModifier { - return new EnemyStatusEffectHealChanceModifier(this.type, this.chance * 100, this.stackCount); - } - - getArgs(): any[] { - return [this.chance * 100]; - } - - /** - * Applies {@linkcode EnemyStatusEffectHealChanceModifier} to randomly heal status. - * @param enemyPokemon - The {@linkcode Pokemon} to heal - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(enemyPokemon: Pokemon): boolean { - if (!enemyPokemon.status || randSeedFloat() > this.chance * this.getStackCount()) { - return false; - } - - globalScene.phaseManager.queueMessage( - getStatusEffectHealText(enemyPokemon.status.effect, getPokemonNameWithAffix(enemyPokemon)), - ); - enemyPokemon.resetStatus(); - enemyPokemon.updateInfo(); - return true; - } - - getMaxStackCount(): number { - return 10; - } -} - -export class EnemyEndureChanceModifier extends EnemyPersistentModifier { - public chance: number; - - constructor(type: ModifierType, _chancePercent?: number, stackCount?: number) { - super(type, stackCount || 10); - - //Hardcode temporarily - this.chance = 2; - } - - match(modifier: Modifier) { - return modifier instanceof EnemyEndureChanceModifier; - } - - clone() { - return new EnemyEndureChanceModifier(this.type, this.chance, this.stackCount); - } - - getArgs(): any[] { - return [this.chance]; - } - - /** - * Applies a chance of enduring a lethal hit of an attack - * @param target the {@linkcode Pokemon} to apply the {@linkcode BattlerTagType.ENDURING} chance to - * @returns `true` if {@linkcode Pokemon} endured - */ - override apply(target: Pokemon): boolean { - if (target.waveData.endured || target.randBattleSeedInt(100) >= this.chance * this.getStackCount()) { - return false; - } - - target.addTag(BattlerTagType.ENDURE_TOKEN, 1); - - target.waveData.endured = true; - - return true; - } - - getMaxStackCount(): number { - return 10; - } -} - -export class EnemyFusionChanceModifier extends EnemyPersistentModifier { - private chance: number; - - constructor(type: ModifierType, chancePercent: number, stackCount?: number) { - super(type, stackCount); - - this.chance = chancePercent / 100; - } - - match(modifier: Modifier) { - return modifier instanceof EnemyFusionChanceModifier && modifier.chance === this.chance; - } - - clone() { - return new EnemyFusionChanceModifier(this.type, this.chance * 100, this.stackCount); - } - - getArgs(): any[] { - return [this.chance * 100]; - } - - /** - * Applies {@linkcode EnemyFusionChanceModifier} - * @param isFusion {@linkcode BooleanHolder} that will be set to `true` if the {@linkcode EnemyPokemon} is a fusion - * @returns `true` if the {@linkcode EnemyPokemon} is a fusion - */ - override apply(isFusion: BooleanHolder): boolean { - if (randSeedFloat() > this.chance * this.getStackCount()) { - return false; - } - - isFusion.value = true; - - return true; - } - - getMaxStackCount(): number { - return 10; - } -} - -/** - * Uses either `MODIFIER_OVERRIDE` in overrides.ts to set {@linkcode PersistentModifier}s for either: - * - The player - * - The enemy - * @param isPlayer {@linkcode boolean} for whether the player (`true`) or enemy (`false`) is being overridden - */ -export function overrideModifiers(isPlayer = true): void { - const modifiersOverride: ModifierOverride[] = isPlayer - ? Overrides.STARTING_MODIFIER_OVERRIDE - : Overrides.OPP_MODIFIER_OVERRIDE; - if (!modifiersOverride || modifiersOverride.length === 0 || !globalScene) { - return; - } - - // If it's the opponent, clear all of their current modifiers to avoid stacking - if (!isPlayer) { - globalScene.clearEnemyModifiers(); - } - - for (const item of modifiersOverride) { - const modifierFunc = modifierTypes[item.name]; - let modifierType: ModifierType | null = modifierFunc(); - - if (modifierType.is("ModifierTypeGenerator")) { - const pregenArgs = "type" in item && item.type !== null ? [item.type] : undefined; - modifierType = modifierType.generateType([], pregenArgs); - } - - const modifier = modifierType && (modifierType.withIdFromFunc(modifierFunc).newModifier() as PersistentModifier); - if (modifier) { - modifier.stackCount = item.count || 1; - - if (isPlayer) { - globalScene.addModifier(modifier, true, false, false, true); - } else { - globalScene.addEnemyModifier(modifier, true, true); - } - } - } -} - -/** - * Uses either `HELD_ITEMS_OVERRIDE` in overrides.ts to set {@linkcode PokemonHeldItemModifier}s for either: - * - The first member of the player's team when starting a new game - * - An enemy {@linkcode Pokemon} being spawned in - * @param pokemon {@linkcode Pokemon} whose held items are being overridden - * @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 - ? Overrides.STARTING_HELD_ITEMS_OVERRIDE - : Overrides.OPP_HELD_ITEMS_OVERRIDE; - if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) { - return; - } - - if (!isPlayer) { - globalScene.clearEnemyHeldItemModifiers(pokemon); - } - - 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); - } - } - } -} - -/** - * Private map from modifier strings to their constructors. - * - * @remarks - * Used for {@linkcode Modifier.is} to check if a modifier is of a certain type without - * requiring modifier types to be imported in every file. - */ -const ModifierClassMap = Object.freeze({ - PersistentModifier, - ConsumableModifier, - AddPokeballModifier, - AddVoucherModifier, - LapsingPersistentModifier, - DoubleBattleChanceBoosterModifier, - TempStatStageBoosterModifier, - TempCritBoosterModifier, - MapModifier, - MegaEvolutionAccessModifier, - GigantamaxAccessModifier, - TerastallizeAccessModifier, - PokemonHeldItemModifier, - LapsingPokemonHeldItemModifier, - BaseStatModifier, - EvoTrackerModifier, - PokemonBaseStatTotalModifier, - PokemonBaseStatFlatModifier, - PokemonIncrementingStatModifier, - StatBoosterModifier, - SpeciesStatBoosterModifier, - CritBoosterModifier, - SpeciesCritBoosterModifier, - AttackTypeBoosterModifier, - SurviveDamageModifier, - BypassSpeedChanceModifier, - FlinchChanceModifier, - TurnHealModifier, - TurnStatusEffectModifier, - HitHealModifier, - LevelIncrementBoosterModifier, - BerryModifier, - PreserveBerryModifier, - PokemonInstantReviveModifier, - ResetNegativeStatStageModifier, - FieldEffectModifier, - ConsumablePokemonModifier, - TerrastalizeModifier, - PokemonHpRestoreModifier, - PokemonStatusHealModifier, - ConsumablePokemonMoveModifier, - PokemonPpRestoreModifier, - PokemonAllMovePpRestoreModifier, - PokemonPpUpModifier, - PokemonNatureChangeModifier, - PokemonLevelIncrementModifier, - TmModifier, - RememberMoveModifier, - EvolutionItemModifier, - FusePokemonModifier, - MultipleParticipantExpBonusModifier, - HealingBoosterModifier, - ExpBoosterModifier, - PokemonExpBoosterModifier, - ExpShareModifier, - ExpBalanceModifier, - PokemonFriendshipBoosterModifier, - PokemonNatureWeightModifier, - PokemonMoveAccuracyBoosterModifier, - PokemonMultiHitModifier, - PokemonFormChangeItemModifier, - MoneyRewardModifier, - DamageMoneyRewardModifier, - MoneyInterestModifier, - HiddenAbilityRateBoosterModifier, - ShinyRateBoosterModifier, - CriticalCatchChanceBoosterModifier, - LockModifierTiersModifier, - HealShopCostModifier, - BoostBugSpawnModifier, - SwitchEffectTransferModifier, - HeldItemTransferModifier, - TurnHeldItemTransferModifier, - ContactHeldItemTransferChanceModifier, - IvScannerModifier, - ExtraModifierModifier, - TempExtraModifierModifier, - EnemyPersistentModifier, - EnemyDamageMultiplierModifier, - EnemyDamageBoosterModifier, - EnemyDamageReducerModifier, - EnemyTurnHealModifier, - EnemyAttackStatusEffectChanceModifier, - EnemyStatusEffectHealChanceModifier, - EnemyEndureChanceModifier, - EnemyFusionChanceModifier, - MoneyMultiplierModifier, -}); - -export type ModifierConstructorMap = typeof ModifierClassMap; diff --git a/src/overrides.ts b/src/overrides.ts index de0d1d3f30a..a21ae28b3a2 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -21,8 +21,10 @@ import { TrainerType } from "#enums/trainer-type"; import { Unlockables } from "#enums/unlockables"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; -import { type ModifierOverride } from "#modifiers/modifier-type"; +import { HeldItemConfiguration } from "#items/held-item-data-types"; +import { TrainerItemConfiguration } from "#items/trainer-item-data-types"; import { Variant } from "#sprites/variant"; +import { RewardSpecs } from "#types/rewards"; /** * This comment block exists to prevent IDEs from automatically removing unused imports @@ -246,16 +248,16 @@ class DefaultOverrides { // ------------------------- /** * Overrides labeled `MODIFIER` deal with any modifier so long as it doesn't require a party - * member to hold it (typically this is, extends, or generates a {@linkcode ModifierType}), + * member to hold it (typically this is, extends, or generates a {@linkcode Reward}), * like `EXP_SHARE`, `CANDY_JAR`, etc. * - * Overrides labeled `HELD_ITEM` specifically pertain to any entry in {@linkcode modifierTypes} that - * extends, or generates a {@linkcode PokemonHeldItemModifierType}, like `SOUL_DEW`, `TOXIC_ORB`, etc. + * Overrides labeled `HELD_ITEM` specifically pertain to any entry in {@linkcode allRewards} that + * extends, or generates a {@linkcode PokemonHeldItemReward}, like `SOUL_DEW`, `TOXIC_ORB`, etc. * * Note that, if count is not provided, it will default to 1. * * Additionally, note that some held items and modifiers are grouped together via - * a {@linkcode ModifierTypeGenerator} and require pre-generation arguments to get + * a {@linkcode RewardGenerator} and require pre-generation arguments to get * a specific item from that group. If a type is not set, the generator will either * use the party to weight item choice or randomly pick an item. * @@ -271,28 +273,18 @@ class DefaultOverrides { * STARTING_HELD_ITEM_OVERRIDE = [{name: "BERRY"}] * ``` */ - readonly STARTING_MODIFIER_OVERRIDE: ModifierOverride[] = []; - /** - * Override array of {@linkcode ModifierOverride}s used to provide modifiers to enemies. - * - * Note that any previous modifiers are cleared. - */ - readonly OPP_MODIFIER_OVERRIDE: ModifierOverride[] = []; - - /** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */ - readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; - /** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */ - readonly OPP_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; + readonly STARTING_TRAINER_ITEMS_OVERRIDE: TrainerItemConfiguration = []; + readonly OPP_TRAINER_ITEMS_OVERRIDE: TrainerItemConfiguration = []; + readonly STARTING_HELD_ITEMS_OVERRIDE: HeldItemConfiguration = []; + readonly OPP_HELD_ITEMS_OVERRIDE: HeldItemConfiguration = []; /** - * Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave. - * * If less entries are listed than rolled, only those entries will be used to replace the corresponding items while the rest randomly generated. * If more entries are listed than rolled, only the first X entries will be used, where X is the number of items rolled. * * Note that, for all items in the array, `count` is not used. */ - readonly ITEM_REWARD_OVERRIDE: ModifierOverride[] = []; + readonly REWARD_OVERRIDE: RewardSpecs[] = []; /** * If `true`, disable all non-scripted opponent trainer encounters. diff --git a/src/phase-manager.ts b/src/phase-manager.ts index aa01a0ffc10..be2045cba18 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -4,7 +4,7 @@ import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/pha import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; import type { Pokemon } from "#field/pokemon"; import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase"; -import { AddEnemyBuffModifierPhase } from "#phases/add-enemy-buff-modifier-phase"; +import { AddEnemyTokenPhase } from "#phases/add-enemy-token-phase"; import { AttemptCapturePhase } from "#phases/attempt-capture-phase"; import { AttemptRunPhase } from "#phases/attempt-run-phase"; import { BattleEndPhase } from "#phases/battle-end-phase"; @@ -26,8 +26,8 @@ import { EvolutionPhase } from "#phases/evolution-phase"; import { ExpPhase } from "#phases/exp-phase"; import { FaintPhase } from "#phases/faint-phase"; import { FormChangePhase } from "#phases/form-change-phase"; -import { GameOverModifierRewardPhase } from "#phases/game-over-modifier-reward-phase"; import { GameOverPhase } from "#phases/game-over-phase"; +import { GameOverRewardPhase } from "#phases/game-over-reward-phase"; import { HideAbilityPhase } from "#phases/hide-ability-phase"; import { HidePartyExpBarPhase } from "#phases/hide-party-exp-bar-phase"; import { LearnMovePhase } from "#phases/learn-move-phase"; @@ -36,7 +36,6 @@ import { LevelUpPhase } from "#phases/level-up-phase"; import { LoadMoveAnimPhase } from "#phases/load-move-anim-phase"; import { LoginPhase } from "#phases/login-phase"; import { MessagePhase } from "#phases/message-phase"; -import { ModifierRewardPhase } from "#phases/modifier-reward-phase"; import { MoneyRewardPhase } from "#phases/money-reward-phase"; import { MoveAnimPhase } from "#phases/move-anim-phase"; import { MoveChargePhase } from "#phases/move-charge-phase"; @@ -70,12 +69,13 @@ import { ReloadSessionPhase } from "#phases/reload-session-phase"; import { ResetStatusPhase } from "#phases/reset-status-phase"; import { ReturnPhase } from "#phases/return-phase"; import { RevivalBlessingPhase } from "#phases/revival-blessing-phase"; -import { RibbonModifierRewardPhase } from "#phases/ribbon-modifier-reward-phase"; +import { RewardPhase } from "#phases/reward-phase"; +import { RibbonRewardPhase } from "#phases/ribbon-reward-phase"; import { ScanIvsPhase } from "#phases/scan-ivs-phase"; import { SelectBiomePhase } from "#phases/select-biome-phase"; import { SelectChallengePhase } from "#phases/select-challenge-phase"; import { SelectGenderPhase } from "#phases/select-gender-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import { SelectTargetPhase } from "#phases/select-target-phase"; import { ShinySparklePhase } from "#phases/shiny-sparkle-phase"; @@ -118,7 +118,7 @@ import { type Constructor, coerceArray } from "#utils/common"; */ const PHASES = Object.freeze({ ActivatePriorityQueuePhase, - AddEnemyBuffModifierPhase, + AddEnemyTokenPhase, AttemptCapturePhase, AttemptRunPhase, BattleEndPhase, @@ -141,7 +141,7 @@ const PHASES = Object.freeze({ FaintPhase, FormChangePhase, GameOverPhase, - GameOverModifierRewardPhase, + GameOverRewardPhase, HideAbilityPhase, HidePartyExpBarPhase, LearnMovePhase, @@ -150,7 +150,7 @@ const PHASES = Object.freeze({ LoadMoveAnimPhase, LoginPhase, MessagePhase, - ModifierRewardPhase, + RewardPhase, MoneyRewardPhase, MoveAnimPhase, MoveChargePhase, @@ -182,12 +182,12 @@ const PHASES = Object.freeze({ ResetStatusPhase, ReturnPhase, RevivalBlessingPhase, - RibbonModifierRewardPhase, + RibbonRewardPhase, ScanIvsPhase, SelectBiomePhase, SelectChallengePhase, SelectGenderPhase, - SelectModifierPhase, + SelectRewardPhase, SelectStarterPhase, SelectTargetPhase, ShinySparklePhase, diff --git a/src/phases/add-enemy-buff-modifier-phase.ts b/src/phases/add-enemy-buff-modifier-phase.ts deleted file mode 100644 index 5c9a56796d4..00000000000 --- a/src/phases/add-enemy-buff-modifier-phase.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import { Phase } from "#app/phase"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { ModifierTier } from "#enums/modifier-tier"; -import { EnemyPersistentModifier } from "#modifiers/modifier"; -import { getEnemyBuffModifierForWave, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; - -export class AddEnemyBuffModifierPhase extends Phase { - public readonly phaseName = "AddEnemyBuffModifierPhase"; - start() { - super.start(); - - const waveIndex = globalScene.currentBattle.waveIndex; - const tier = !(waveIndex % 1000) - ? ModifierTier.ULTRA - : !(waveIndex % 250) - ? ModifierTier.GREAT - : ModifierTier.COMMON; - - regenerateModifierPoolThresholds(globalScene.getEnemyParty(), ModifierPoolType.ENEMY_BUFF); - - const count = Math.ceil(waveIndex / 250); - for (let i = 0; i < count; i++) { - globalScene.addEnemyModifier( - getEnemyBuffModifierForWave( - tier, - globalScene.findModifiers(m => m instanceof EnemyPersistentModifier, false), - ), - true, - true, - ); - } - globalScene.updateModifiers(false, true); - this.end(); - } -} diff --git a/src/phases/add-enemy-token-phase.ts b/src/phases/add-enemy-token-phase.ts new file mode 100644 index 00000000000..af617b49bb4 --- /dev/null +++ b/src/phases/add-enemy-token-phase.ts @@ -0,0 +1,21 @@ +import { globalScene } from "#app/global-scene"; +import { Phase } from "#app/phase"; +import { RarityTier } from "#enums/reward-tier"; +import { assignEnemyBuffTokenForWave } from "#items/trainer-item-pool"; + +export class AddEnemyTokenPhase extends Phase { + public readonly phaseName = "AddEnemyTokenPhase"; + start() { + super.start(); + + const waveIndex = globalScene.currentBattle.waveIndex; + const tier = !(waveIndex % 1000) ? RarityTier.ULTRA : !(waveIndex % 250) ? RarityTier.GREAT : RarityTier.COMMON; + + const count = Math.ceil(waveIndex / 250); + for (let i = 0; i < count; i++) { + assignEnemyBuffTokenForWave(tier); + } + globalScene.updateItems(false); + this.end(); + } +} diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index fcddd23dd20..49f6c7d41d5 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -17,7 +17,6 @@ import { StatusEffect } from "#enums/status-effect"; import { UiMode } from "#enums/ui-mode"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#field/anims"; import type { EnemyPokemon } from "#field/pokemon"; -import { PokemonHeldItemModifier } from "#modifiers/modifier"; import { PokemonPhase } from "#phases/pokemon-phase"; import { achvs } from "#system/achv"; import type { PartyOption } from "#ui/party-ui-handler"; @@ -256,6 +255,7 @@ export class AttemptCapturePhase extends PokemonPhase { }), null, () => { + const heldItemConfig = pokemon.heldItemManager.generateHeldItemConfiguration(); const end = () => { globalScene.phaseManager.unshiftNew("VictoryPhase", this.battlerIndex); globalScene.pokemonInfoContainer.hide(); @@ -266,25 +266,21 @@ 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.leaveField(true, true, false); - newPokemon.loadAssets().then(end); - } else { - end(); - } - }); + globalScene.updateItems(true); + removePokemon(); + if (newPokemon) { + newPokemon.leaveField(true, true, false); + newPokemon.loadAssets().then(end); + } else { + end(); + } }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { @@ -309,6 +305,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 a59667bdd4e..ef3adc03c5c 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -40,8 +40,6 @@ export class AttemptRunPhase extends FieldPhase { onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy()), }); - globalScene.clearEnemyHeldItemModifiers(); - enemyField.forEach(enemyPokemon => { enemyPokemon.hideInfo().then(() => enemyPokemon.destroy()); enemyPokemon.hp = 0; diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 8d199915385..8cd021f3683 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -1,6 +1,5 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; -import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#modifiers/modifier"; import { BattlePhase } from "#phases/battle-phase"; export class BattleEndPhase extends BattlePhase { @@ -72,7 +71,6 @@ export class BattleEndPhase extends BattlePhase { globalScene.currentBattle.pickUpScatteredMoney(); } - globalScene.clearEnemyHeldItemModifiers(); for (const p of globalScene.getEnemyParty()) { try { p.destroy(); @@ -81,20 +79,9 @@ export class BattleEndPhase extends BattlePhase { } } - const lapsingModifiers = globalScene.findModifiers( - m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier, - ) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[]; - for (const m of lapsingModifiers) { - const args: any[] = []; - if (m instanceof LapsingPokemonHeldItemModifier) { - args.push(globalScene.getPokemonById(m.pokemonId)); - } - if (!m.lapse(...args)) { - globalScene.removeModifier(m); - } - } + globalScene.trainerItems.lapseItems(); - globalScene.updateModifiers(); + globalScene.updateItems(); this.end(); } } diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index 941406d0b96..fd8783a9e28 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -1,10 +1,13 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; +import { allHeldItems } from "#data/data-lists"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemCategoryId, isItemInCategory } from "#enums/held-item-id"; import { CommonAnim } from "#enums/move-anims-common"; -import { BerryUsedEvent } from "#events/battle-scene"; import type { Pokemon } from "#field/pokemon"; -import { BerryModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; +import type { BerryHeldItem } from "#items/berry"; import { FieldPhase } from "#phases/field-phase"; import { BooleanHolder } from "#utils/common"; import i18next from "i18next"; @@ -31,10 +34,9 @@ export class BerryPhase extends FieldPhase { * @param pokemon - The {@linkcode Pokemon} to check */ eatBerries(pokemon: Pokemon): void { - const hasUsableBerry = !!globalScene.findModifier( - m => m instanceof BerryModifier && m.shouldApply(pokemon), - pokemon.isPlayer(), - ); + const hasUsableBerry = pokemon.getHeldItems().some(m => { + return isItemInCategory(m, HeldItemCategoryId.BERRY) && (allHeldItems[m] as BerryHeldItem).shouldApply(pokemon); + }); if (!hasUsableBerry) { return; @@ -59,15 +61,8 @@ export class BerryPhase extends FieldPhase { CommonAnim.USE_ITEM, ); - for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) { - // No need to track berries being eaten; already done inside applyModifiers - if (berryModifier.consumed) { - berryModifier.consumed = false; - pokemon.loseHeldItem(berryModifier); - } - globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); - } - globalScene.updateModifiers(pokemon.isPlayer()); + applyHeldItems(HeldItemEffect.BERRY, { pokemon: pokemon }); + globalScene.updateItems(pokemon.isPlayer()); // AbilityId.CHEEK_POUCH only works once per round of nom noms applyAbAttrs("HealFromBerryUseAbAttr", { pokemon }); diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 94923ae8c1f..30dca435637 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -228,7 +228,7 @@ export class EggHatchPhase extends Phase { if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) { this.eggHatchHandler.clear(); } else { - globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); + globalScene.time.delayedCall(250, () => globalScene.setItemsVisible(true)); } super.end(); } diff --git a/src/phases/egg-summary-phase.ts b/src/phases/egg-summary-phase.ts index c236c5c3abc..94df2d1143c 100644 --- a/src/phases/egg-summary-phase.ts +++ b/src/phases/egg-summary-phase.ts @@ -39,7 +39,7 @@ export class EggSummaryPhase extends Phase { } end() { - globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); + globalScene.time.delayedCall(250, () => globalScene.setItemsVisible(true)); globalScene.ui.setModeForceTransition(UiMode.MESSAGE).then(() => { super.end(); }); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 79da7134e9a..81a9ef649ab 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -13,22 +13,15 @@ import { BattleType } from "#enums/battle-type"; import { BattlerIndex } from "#enums/battler-index"; import { BiomeId } from "#enums/biome-id"; import { FieldPosition } from "#enums/field-position"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { PlayerGender } from "#enums/player-gender"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { TrainerSlot } from "#enums/trainer-slot"; import { UiMode } from "#enums/ui-mode"; import { EncounterPhaseEvent } from "#events/battle-scene"; import type { Pokemon } from "#field/pokemon"; -import { - BoostBugSpawnModifier, - IvScannerModifier, - overrideHeldItems, - overrideModifiers, - TurnHeldItemTransferModifier, -} from "#modifiers/modifier"; -import { regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; +import { overrideHeldItems, overrideTrainerItems } from "#items/item-overrides"; import { getEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { doTrainerExclamation } from "#mystery-encounters/encounter-phase-utils"; import { getGoldenBugNetSpecies } from "#mystery-encounters/encounter-pokemon-utils"; @@ -111,7 +104,7 @@ export class EncounterPhase extends BattlePhase { let enemySpecies = globalScene.randomSpecies(battle.waveIndex, level, true); // If player has golden bug net, rolls 10% chance to replace non-boss wave wild species from the golden bug net bug pool if ( - globalScene.findModifier(m => m instanceof BoostBugSpawnModifier) && + globalScene.trainerItems.hasItem(TrainerItemId.GOLDEN_BUG_NET) && !globalScene.gameMode.isBoss(battle.waveIndex) && globalScene.arena.biomeType !== BiomeId.END && randSeedInt(10) === 0 @@ -275,12 +268,8 @@ export class EncounterPhase extends BattlePhase { if (!this.loaded && battle.battleType !== BattleType.MYSTERY_ENCOUNTER) { // generate modifiers for MEs, overriding prior ones as applicable - regenerateModifierPoolThresholds( - globalScene.getEnemyField(), - battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, - ); - globalScene.generateEnemyModifiers(); - overrideModifiers(false); + globalScene.generateEnemyItems(); + overrideTrainerItems(false); for (const enemy of globalScene.getEnemyField()) { overrideHeldItems(enemy, false); @@ -315,7 +304,7 @@ export class EncounterPhase extends BattlePhase { doEncounter() { globalScene.playBgm(undefined, true); - globalScene.updateModifiers(false); + globalScene.updateItems(false); globalScene.setFieldScale(1); const { battleType, waveIndex } = globalScene.currentBattle; @@ -353,6 +342,7 @@ export class EncounterPhase extends BattlePhase { } }, }); + globalScene.updateItems(false); const encounterIntroVisuals = globalScene.currentBattle?.mysteryEncounter?.introVisuals; if (encounterIntroVisuals) { @@ -547,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)) { @@ -589,8 +563,7 @@ export class EncounterPhase extends BattlePhase { }, ), ); - const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); - if (ivScannerModifier) { + if (globalScene.trainerItems.hasItem(TrainerItemId.IV_SCANNER)) { enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); } } diff --git a/src/phases/exp-phase.ts b/src/phases/exp-phase.ts index 7ab162c2ac6..4ab58bdbf9c 100644 --- a/src/phases/exp-phase.ts +++ b/src/phases/exp-phase.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import { ExpBoosterModifier } from "#modifiers/modifier"; +import { TrainerItemEffect } from "#items/trainer-item"; import { PlayerPartyMemberPokemonPhase } from "#phases/player-party-member-pokemon-phase"; import { NumberHolder } from "#utils/common"; import i18next from "i18next"; @@ -20,7 +20,7 @@ export class ExpPhase extends PlayerPartyMemberPokemonPhase { const pokemon = this.getPokemon(); const exp = new NumberHolder(this.expValue); - globalScene.applyModifiers(ExpBoosterModifier, true, exp); + globalScene.applyPlayerItems(TrainerItemEffect.EXP_BOOSTER, { numberHolder: exp }); exp.value = Math.floor(exp.value); globalScene.ui.showText( i18next.t("battle:expGain", { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index d1bd0ed0804..504d2bc2b0d 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -10,11 +10,12 @@ import { BattleType } from "#enums/battle-type"; import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemEffect } from "#enums/held-item-effect"; import { HitResult } from "#enums/hit-result"; import { StatusEffect } from "#enums/status-effect"; import { SwitchType } from "#enums/switch-type"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; -import { PokemonInstantReviveModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import { PokemonMove } from "#moves/pokemon-move"; import { PokemonPhase } from "#phases/pokemon-phase"; import { isNullOrUndefined } from "#utils/common"; @@ -52,17 +53,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(HeldItemEffect.INSTANT_REVIVE, { pokemon: faintPokemon }); } /** diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index d4562b5a237..debb9aacb86 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -2,7 +2,7 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { clientSessionId } from "#app/account"; import { globalScene } from "#app/global-scene"; import { pokemonEvolutions } from "#balance/pokemon-evolutions"; -import { modifierTypes } from "#data/data-lists"; +import { allRewards } from "#data/data-lists"; import { getCharVariantFromDialogue } from "#data/dialogue"; import type { PokemonSpecies } from "#data/pokemon-species"; import { BattleType } from "#enums/battle-type"; @@ -17,7 +17,6 @@ import { achvs, ChallengeAchv } from "#system/achv"; import { ArenaData } from "#system/arena-data"; import { ChallengeData } from "#system/challenge-data"; import type { SessionSaveData } from "#system/game-data"; -import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; import { TrainerData } from "#system/trainer-data"; import { trainerConfigs } from "#trainers/trainer-config"; @@ -152,10 +151,10 @@ export class GameOverPhase extends BattlePhase { this.handleUnlocks(); for (const species of this.firstRibbons) { - globalScene.phaseManager.unshiftNew("RibbonModifierRewardPhase", modifierTypes.VOUCHER_PLUS, species); + globalScene.phaseManager.unshiftNew("RibbonRewardPhase", allRewards.VOUCHER_PLUS, species); } if (!firstClear) { - globalScene.phaseManager.unshiftNew("GameOverModifierRewardPhase", modifierTypes.VOUCHER_PREMIUM); + globalScene.phaseManager.unshiftNew("GameOverRewardPhase", allRewards.VOUCHER_PREMIUM); } } this.getRunHistoryEntry().then(runHistoryEntry => { @@ -285,12 +284,10 @@ export class GameOverPhase extends BattlePhase { gameMode: globalScene.gameMode.modeId, party: globalScene.getPlayerParty().map(p => new PokemonData(p)), enemyParty: globalScene.getEnemyParty().map(p => new PokemonData(p)), - modifiers: preWaveSessionData - ? preWaveSessionData.modifiers - : globalScene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), - enemyModifiers: preWaveSessionData - ? preWaveSessionData.enemyModifiers - : globalScene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), + trainerItems: preWaveSessionData ? preWaveSessionData.trainerItems : globalScene.trainerItems.generateSaveData(), + enemyTrainerItems: preWaveSessionData + ? preWaveSessionData.enemyTrainerItems + : globalScene.enemyTrainerItems.generateSaveData(), arena: new ArenaData(globalScene.arena), pokeballCounts: globalScene.pokeballCounts, money: Math.floor(globalScene.money), diff --git a/src/phases/game-over-modifier-reward-phase.ts b/src/phases/game-over-reward-phase.ts similarity index 75% rename from src/phases/game-over-modifier-reward-phase.ts rename to src/phases/game-over-reward-phase.ts index f07c7d2adba..f8a15e05185 100644 --- a/src/phases/game-over-modifier-reward-phase.ts +++ b/src/phases/game-over-reward-phase.ts @@ -1,13 +1,13 @@ import { globalScene } from "#app/global-scene"; import { UiMode } from "#enums/ui-mode"; -import { ModifierRewardPhase } from "#phases/modifier-reward-phase"; +import { RewardPhase } from "#phases/reward-phase"; import i18next from "i18next"; -export class GameOverModifierRewardPhase extends ModifierRewardPhase { - public readonly phaseName = "GameOverModifierRewardPhase"; +export class GameOverRewardPhase extends RewardPhase { + public readonly phaseName = "GameOverRewardPhase"; doReward(): Promise { return new Promise(resolve => { - const newModifier = this.modifierType.newModifier(); + const newModifier = this.reward.newModifier(); globalScene.addModifier(newModifier); // Sound loaded into game as is globalScene.playSound("level_up_fanfare"); diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts index a714d247730..08be8502a3a 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -187,7 +187,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { pokemon.usedTMs = []; } pokemon.usedTMs.push(this.moveId); - globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase")); + globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectRewardPhase")); } else if (this.learnMoveType === LearnMoveType.MEMORY) { if (this.cost !== -1) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { @@ -197,7 +197,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { } globalScene.playSound("se/buy"); } else { - globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase")); + globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectRewardPhase")); } } pokemon.setMove(index, this.moveId); diff --git a/src/phases/modifier-reward-phase.ts b/src/phases/modifier-reward-phase.ts deleted file mode 100644 index b84985b7311..00000000000 --- a/src/phases/modifier-reward-phase.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import type { ModifierType } from "#modifiers/modifier-type"; -import { BattlePhase } from "#phases/battle-phase"; -import type { ModifierTypeFunc } from "#types/modifier-types"; -import { getModifierType } from "#utils/modifier-utils"; -import i18next from "i18next"; - -export class ModifierRewardPhase extends BattlePhase { - // RibbonModifierRewardPhase extends ModifierRewardPhase and to make typescript happy - // we need to use a union type here - public readonly phaseName: "ModifierRewardPhase" | "RibbonModifierRewardPhase" | "GameOverModifierRewardPhase" = - "ModifierRewardPhase"; - protected modifierType: ModifierType; - - constructor(modifierTypeFunc: ModifierTypeFunc) { - super(); - - this.modifierType = getModifierType(modifierTypeFunc); - } - - start() { - super.start(); - - this.doReward().then(() => this.end()); - } - - doReward(): Promise { - return new Promise(resolve => { - const newModifier = this.modifierType.newModifier(); - globalScene.addModifier(newModifier); - globalScene.playSound("item_fanfare"); - globalScene.ui.showText( - i18next.t("battle:rewardGain", { - modifierName: newModifier?.type.name, - }), - null, - () => resolve(), - null, - true, - ); - }); - } -} diff --git a/src/phases/money-reward-phase.ts b/src/phases/money-reward-phase.ts index 61153de8565..1b6b4ded6b1 100644 --- a/src/phases/money-reward-phase.ts +++ b/src/phases/money-reward-phase.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import { ArenaTagType } from "#enums/arena-tag-type"; -import { MoneyMultiplierModifier } from "#modifiers/modifier"; +import { TrainerItemEffect } from "#items/trainer-item"; import { BattlePhase } from "#phases/battle-phase"; import { NumberHolder } from "#utils/common"; import i18next from "i18next"; @@ -18,7 +18,7 @@ export class MoneyRewardPhase extends BattlePhase { start() { const moneyAmount = new NumberHolder(globalScene.getWaveMoneyAmount(this.moneyMultiplier)); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); + globalScene.applyPlayerItems(TrainerItemEffect.MONEY_MULTIPLIER, { numberHolder: moneyAmount }); if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) { moneyAmount.value *= 2; diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index c57e0f6cead..aa3ba1b68be 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -11,6 +11,7 @@ import { ArenaTagSide } from "#enums/arena-tag-side"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemEffect } from "#enums/held-item-effect"; import { HitCheckResult } from "#enums/hit-check-result"; import { HitResult } from "#enums/hit-result"; import { MoveCategory } from "#enums/move-category"; @@ -22,15 +23,8 @@ import { MoveTarget } from "#enums/move-target"; import { isReflected, MoveUseMode } from "#enums/move-use-mode"; import { PokemonType } from "#enums/pokemon-type"; import type { Pokemon } from "#field/pokemon"; -import { - ContactHeldItemTransferChanceModifier, - DamageMoneyRewardModifier, - EnemyAttackStatusEffectChanceModifier, - EnemyEndureChanceModifier, - FlinchChanceModifier, - HitHealModifier, - PokemonMultiHitModifier, -} from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; +import { TrainerItemEffect } from "#items/trainer-item"; import { applyFilteredMoveAttrs, applyMoveAttrs } from "#moves/apply-attrs"; import type { Move, MoveAttr } from "#moves/move"; import { getMoveTargets, isFieldTargeted } from "#moves/move-utils"; @@ -287,7 +281,7 @@ export class MoveEffectPhase extends PokemonPhase { // If Parental Bond is applicable, add another hit applyAbAttrs("AddSecondStrikeAbAttr", { pokemon: user, move, hitCount }); // If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses - globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount); + applyHeldItems(HeldItemEffect.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; @@ -387,7 +381,7 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); } - globalScene.applyModifiers(HitHealModifier, this.player, user); + applyHeldItems(HeldItemEffect.HIT_HEAL, { pokemon: user }); this.getTargets().forEach(target => { target.turnData.moveEffectiveness = null; }); @@ -429,7 +423,7 @@ export class MoveEffectPhase extends PokemonPhase { !this.move.hitsSubstitute(user, target) ) { const flinched = new BooleanHolder(false); - globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + applyHeldItems(HeldItemEffect.FLINCH_CHANCE, { pokemon: user, flinched: flinched }); if (flinched.value) { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.id, user.id); } @@ -831,7 +825,7 @@ export class MoveEffectPhase extends PokemonPhase { if (isBlockedBySubstitute) { substitute.hp -= dmg; } else if (!target.isPlayer() && dmg >= target.hp) { - globalScene.applyModifiers(EnemyEndureChanceModifier, false, target); + globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_ENDURE_CHANCE, { pokemon: target }); } const damage = isBlockedBySubstitute @@ -875,7 +869,7 @@ export class MoveEffectPhase extends PokemonPhase { }); if (user.isPlayer() && target.isEnemy()) { - globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage)); + applyHeldItems(HeldItemEffect.DAMAGE_MONEY_REWARD, { pokemon: user, damage: damage }); } return [result, isCritical]; @@ -983,12 +977,12 @@ export class MoveEffectPhase extends PokemonPhase { // We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens if (!user.isPlayer() && this.move.is("AttackMove")) { - globalScene.applyShuffledModifiers(EnemyAttackStatusEffectChanceModifier, false, target); + globalScene.applyShuffledStatusTokens(target); } // Apply Grip Claw's chance to steal an item from the target if (this.move.is("AttackMove")) { - globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); + applyHeldItems(HeldItemEffect.CONTACT_ITEM_STEAL_CHANCE, { pokemon: user, target: target }); } } } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 9363efcb460..4f9c51974a1 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -6,9 +6,9 @@ import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { SwitchType } from "#enums/switch-type"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { TrainerSlot } from "#enums/trainer-slot"; import { UiMode } from "#enums/ui-mode"; -import { IvScannerModifier } from "#modifiers/modifier"; import { getEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { OptionSelectSettings } from "#mystery-encounters/encounter-phase-utils"; import { transitionMysteryEncounterIntroVisuals } from "#mystery-encounters/encounter-phase-utils"; @@ -413,8 +413,7 @@ export class MysteryEncounterBattlePhase extends Phase { // PostSummon and ShinySparkle phases are handled by SummonPhase if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE) { - const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); - if (ivScannerModifier) { + if (globalScene.trainerItems.hasItem(TrainerItemId.IV_SCANNER)) { enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); } } @@ -533,13 +532,13 @@ export class MysteryEncounterRewardsPhase extends Phase { } else { this.doEncounterRewardsAndContinue(); } - // Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave) + // Do not use ME's seedOffset for allRewards, these should always be consistent with waveIndex (once per wave) }, globalScene.currentBattle.waveIndex * 1000); } } /** - * Queues encounter EXP and rewards phases, {@linkcode PostMysteryEncounterPhase}, and ends phase + * Queues encounter EXP and allRewards phases, {@linkcode PostMysteryEncounterPhase}, and ends phase */ doEncounterRewardsAndContinue() { const encounter = globalScene.currentBattle.mysteryEncounter!; @@ -551,8 +550,8 @@ export class MysteryEncounterRewardsPhase extends Phase { if (encounter.doEncounterRewards) { encounter.doEncounterRewards(); } else if (this.addHealPhase) { - globalScene.phaseManager.tryRemovePhase(p => p.is("SelectModifierPhase")); - globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, { + globalScene.phaseManager.tryRemovePhase(p => p.is("SelectRewardPhase")); + globalScene.phaseManager.unshiftNew("SelectRewardPhase", 0, undefined, { fillRemaining: false, rerollMultiplier: -1, }); diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index fa6a3222466..4892bf685fe 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -7,7 +7,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { HitResult } from "#enums/hit-result"; import { CommonAnim } from "#enums/move-anims-common"; import { StatusEffect } from "#enums/status-effect"; -import { HealingBoosterModifier } from "#modifiers/modifier"; +import { TrainerItemEffect } from "#items/trainer-item"; import { CommonAnimPhase } from "#phases/common-anim-phase"; import { HealAchv } from "#system/achv"; import { NumberHolder } from "#utils/common"; @@ -75,7 +75,7 @@ export class PokemonHealPhase extends CommonAnimPhase { if (healOrDamage) { const hpRestoreMultiplier = new NumberHolder(1); if (!this.revive) { - globalScene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); + globalScene.applyPlayerItems(TrainerItemEffect.HEALING_BOOSTER, { numberHolder: hpRestoreMultiplier }); } const healAmount = new NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value)); if (healAmount.value < 0) { diff --git a/src/phases/reward-phase.ts b/src/phases/reward-phase.ts new file mode 100644 index 00000000000..033a022958f --- /dev/null +++ b/src/phases/reward-phase.ts @@ -0,0 +1,40 @@ +import { globalScene } from "#app/global-scene"; +import type { Reward } from "#items/reward"; +import { BattlePhase } from "#phases/battle-phase"; +import type { RewardFunc } from "#types/rewards"; +import i18next from "i18next"; + +export class RewardPhase extends BattlePhase { + // RibbonRewardPhase extends RewardPhase and to make typescript happy + // we need to use a union type here + public readonly phaseName: "RewardPhase" | "RibbonRewardPhase" | "GameOverRewardPhase" = "RewardPhase"; + protected reward: Reward; + + constructor(rewardFunc: RewardFunc) { + super(); + + this.reward = rewardFunc(); + } + + start() { + super.start(); + + this.doReward().then(() => this.end()); + } + + doReward(): Promise { + return new Promise(resolve => { + globalScene.applyReward(this.reward); + globalScene.playSound("item_fanfare"); + globalScene.ui.showText( + i18next.t("battle:rewardGain", { + modifierName: this.reward.name, + }), + null, + () => resolve(), + null, + true, + ); + }); + } +} diff --git a/src/phases/ribbon-modifier-reward-phase.ts b/src/phases/ribbon-reward-phase.ts similarity index 65% rename from src/phases/ribbon-modifier-reward-phase.ts rename to src/phases/ribbon-reward-phase.ts index 7cf3f7a8071..42e5bd826a3 100644 --- a/src/phases/ribbon-modifier-reward-phase.ts +++ b/src/phases/ribbon-reward-phase.ts @@ -1,23 +1,23 @@ import { globalScene } from "#app/global-scene"; import type { PokemonSpecies } from "#data/pokemon-species"; import { UiMode } from "#enums/ui-mode"; -import { ModifierRewardPhase } from "#phases/modifier-reward-phase"; -import type { ModifierTypeFunc } from "#types/modifier-types"; +import { RewardPhase } from "#phases/reward-phase"; +import type { RewardFunc } from "#types/rewards"; import i18next from "i18next"; -export class RibbonModifierRewardPhase extends ModifierRewardPhase { - public readonly phaseName = "RibbonModifierRewardPhase"; +export class RibbonRewardPhase extends RewardPhase { + public readonly phaseName = "RibbonRewardPhase"; private species: PokemonSpecies; - constructor(modifierTypeFunc: ModifierTypeFunc, species: PokemonSpecies) { - super(modifierTypeFunc); + constructor(rewardFunc: RewardFunc, species: PokemonSpecies) { + super(rewardFunc); this.species = species; } doReward(): Promise { return new Promise(resolve => { - const newModifier = this.modifierType.newModifier(); + const newModifier = this.reward.newModifier(); globalScene.addModifier(newModifier); globalScene.playSound("level_up_fanfare"); globalScene.ui.setMode(UiMode.MESSAGE); diff --git a/src/phases/select-biome-phase.ts b/src/phases/select-biome-phase.ts index ab96bf5c45e..15c523e99f5 100644 --- a/src/phases/select-biome-phase.ts +++ b/src/phases/select-biome-phase.ts @@ -1,8 +1,8 @@ import { globalScene } from "#app/global-scene"; import { biomeLinks, getBiomeName } from "#balance/biomes"; import { BiomeId } from "#enums/biome-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; -import { MapModifier, MoneyInterestModifier } from "#modifiers/modifier"; import { BattlePhase } from "#phases/battle-phase"; import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; import { randSeedInt } from "#utils/common"; @@ -19,7 +19,6 @@ export class SelectBiomePhase extends BattlePhase { const setNextBiome = (nextBiome: BiomeId) => { if (nextWaveIndex % 10 === 1) { - globalScene.applyModifiers(MoneyInterestModifier, true); globalScene.phaseManager.unshiftNew("PartyHealPhase", false); } globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome); @@ -39,7 +38,7 @@ export class SelectBiomePhase extends BattlePhase { .filter(b => !Array.isArray(b) || !randSeedInt(b[1])) .map(b => (!Array.isArray(b) ? b : b[0])); - if (biomes.length > 1 && globalScene.findModifier(m => m instanceof MapModifier)) { + if (biomes.length > 1 && globalScene.trainerItems.hasItem(TrainerItemId.MAP)) { const biomeSelectItems = biomes.map(b => { const ret: OptionSelectItem = { label: getBiomeName(b), diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts deleted file mode 100644 index 05c890136ee..00000000000 --- a/src/phases/select-modifier-phase.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import Overrides from "#app/overrides"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; -import type { ModifierTier } from "#enums/modifier-tier"; -import { UiMode } from "#enums/ui-mode"; -import type { Modifier } from "#modifiers/modifier"; -import { - ExtraModifierModifier, - HealShopCostModifier, - PokemonHeldItemModifier, - TempExtraModifierModifier, -} from "#modifiers/modifier"; -import type { CustomModifierSettings, ModifierType, ModifierTypeOption } from "#modifiers/modifier-type"; -import { - FusePokemonModifierType, - getPlayerModifierTypeOptions, - getPlayerShopModifierTypeOptionsForWave, - PokemonModifierType, - PokemonMoveModifierType, - PokemonPpRestoreModifierType, - PokemonPpUpModifierType, - RememberMoveModifierType, - regenerateModifierPoolThresholds, - TmModifierType, -} from "#modifiers/modifier-type"; -import { BattlePhase } from "#phases/battle-phase"; -import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; -import { SHOP_OPTIONS_ROW_LIMIT } from "#ui/modifier-select-ui-handler"; -import { PartyOption, PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; -import { isNullOrUndefined, NumberHolder } from "#utils/common"; -import i18next from "i18next"; - -export type ModifierSelectCallback = (rowCursor: number, cursor: number) => boolean; - -export class SelectModifierPhase extends BattlePhase { - public readonly phaseName = "SelectModifierPhase"; - private rerollCount: number; - private modifierTiers?: ModifierTier[]; - private customModifierSettings?: CustomModifierSettings; - private isCopy: boolean; - - private typeOptions: ModifierTypeOption[]; - - constructor( - rerollCount = 0, - modifierTiers?: ModifierTier[], - customModifierSettings?: CustomModifierSettings, - isCopy = false, - ) { - super(); - - this.rerollCount = rerollCount; - this.modifierTiers = modifierTiers; - this.customModifierSettings = customModifierSettings; - this.isCopy = isCopy; - } - - start() { - super.start(); - - if (!this.isPlayer()) { - return false; - } - - if (!this.rerollCount && !this.isCopy) { - this.updateSeed(); - } else if (this.rerollCount) { - globalScene.reroll = false; - } - - const party = globalScene.getPlayerParty(); - if (!this.isCopy) { - regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); - } - const modifierCount = this.getModifierCount(); - - this.typeOptions = this.getModifierTypeOptions(modifierCount); - - const modifierSelectCallback = (rowCursor: number, cursor: number) => { - if (rowCursor < 0 || cursor < 0) { - globalScene.ui.showText(i18next.t("battle:skipItemQuestion"), null, () => { - globalScene.ui.setOverlayMode( - UiMode.CONFIRM, - () => { - globalScene.ui.revertMode(); - globalScene.ui.setMode(UiMode.MESSAGE); - super.end(); - }, - () => this.resetModifierSelect(modifierSelectCallback), - ); - }); - return false; - } - - switch (rowCursor) { - // Execute one of the options from the bottom row - case 0: - switch (cursor) { - case 0: - return this.rerollModifiers(); - case 1: - return this.openModifierTransferScreen(modifierSelectCallback); - // Check the party, pass a callback to restore the modifier select screen. - case 2: - globalScene.ui.setModeWithoutClear(UiMode.PARTY, PartyUiMode.CHECK, -1, () => { - this.resetModifierSelect(modifierSelectCallback); - }); - return true; - case 3: - return this.toggleRerollLock(); - default: - return false; - } - // Pick an option from the rewards - case 1: - return this.selectRewardModifierOption(cursor, modifierSelectCallback); - // Pick an option from the shop - default: { - return this.selectShopModifierOption(rowCursor, cursor, modifierSelectCallback); - } - } - }; - - this.resetModifierSelect(modifierSelectCallback); - } - - // Pick a modifier from among the rewards and apply it - private selectRewardModifierOption(cursor: number, modifierSelectCallback: ModifierSelectCallback): boolean { - if (this.typeOptions.length === 0) { - globalScene.ui.clearText(); - globalScene.ui.setMode(UiMode.MESSAGE); - super.end(); - return true; - } - const modifierType = this.typeOptions[cursor].type; - return this.applyChosenModifier(modifierType, -1, modifierSelectCallback); - } - - // Pick a modifier from the shop and apply it - private selectShopModifierOption( - rowCursor: number, - cursor: number, - modifierSelectCallback: ModifierSelectCallback, - ): boolean { - const shopOptions = getPlayerShopModifierTypeOptionsForWave( - globalScene.currentBattle.waveIndex, - globalScene.getWaveMoneyAmount(1), - ); - const shopOption = - shopOptions[ - rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT - ]; - const modifierType = shopOption.type; - // Apply Black Sludge to healing item cost - const healingItemCost = new NumberHolder(shopOption.cost); - globalScene.applyModifier(HealShopCostModifier, true, healingItemCost); - const cost = healingItemCost.value; - - if (globalScene.money < cost && !Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - globalScene.ui.playError(); - return false; - } - - return this.applyChosenModifier(modifierType, cost, modifierSelectCallback); - } - - // Apply a chosen modifier: do an effect or open the party menu - private applyChosenModifier( - modifierType: ModifierType, - cost: number, - modifierSelectCallback: ModifierSelectCallback, - ): boolean { - if (modifierType instanceof PokemonModifierType) { - if (modifierType instanceof FusePokemonModifierType) { - this.openFusionMenu(modifierType, cost, modifierSelectCallback); - } else { - this.openModifierMenu(modifierType, cost, modifierSelectCallback); - } - } else { - this.applyModifier(modifierType.newModifier()!); - } - return cost === -1; - } - - // Reroll rewards - private rerollModifiers() { - const rerollCost = this.getRerollCost(globalScene.lockModifierTiers); - if (rerollCost < 0 || globalScene.money < rerollCost) { - globalScene.ui.playError(); - return false; - } - globalScene.reroll = true; - globalScene.phaseManager.unshiftNew( - "SelectModifierPhase", - this.rerollCount + 1, - this.typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[], - ); - globalScene.ui.clearText(); - globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end()); - if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - globalScene.money -= rerollCost; - globalScene.updateMoneyText(); - globalScene.animateMoneyChanged(false); - } - globalScene.playSound("se/buy"); - return true; - } - - // Transfer modifiers among party pokemon - private openModifierTransferScreen(modifierSelectCallback: ModifierSelectCallback) { - const party = globalScene.getPlayerParty(); - globalScene.ui.setModeWithoutClear( - UiMode.PARTY, - PartyUiMode.MODIFIER_TRANSFER, - -1, - (fromSlotIndex: number, itemIndex: number, itemQuantity: number, toSlotIndex: number) => { - if ( - toSlotIndex !== undefined && - fromSlotIndex < 6 && - toSlotIndex < 6 && - 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, - party[toSlotIndex], - true, - itemQuantity, - undefined, - undefined, - false, - ); - } else { - this.resetModifierSelect(modifierSelectCallback); - } - }, - PartyUiHandler.FilterItemMaxStacks, - ); - return true; - } - - // Toggle reroll lock - private toggleRerollLock() { - const rerollCost = this.getRerollCost(globalScene.lockModifierTiers); - if (rerollCost < 0) { - // Reroll lock button is also disabled when reroll is disabled - globalScene.ui.playError(); - return false; - } - globalScene.lockModifierTiers = !globalScene.lockModifierTiers; - const uiHandler = globalScene.ui.getHandler() as ModifierSelectUiHandler; - uiHandler.setRerollCost(this.getRerollCost(globalScene.lockModifierTiers)); - uiHandler.updateLockRaritiesText(); - uiHandler.updateRerollCostText(); - return false; - } - - /** - * Apply the effects of the chosen modifier - * @param modifier - The modifier to apply - * @param cost - The cost of the modifier if it was purchased, or -1 if selected as the modifier reward - * @param playSound - Whether the 'obtain modifier' sound should be played when adding the modifier. - */ - private applyModifier(modifier: Modifier, cost = -1, playSound = false): void { - const result = globalScene.addModifier(modifier, false, playSound, undefined, undefined, cost); - // Queue a copy of this phase when applying a TM or Memory Mushroom. - // If the player selects either of these, then escapes out of consuming them, - // they are returned to a shop in the same state. - if (modifier.type instanceof RememberMoveModifierType || modifier.type instanceof TmModifierType) { - globalScene.phaseManager.unshiftPhase(this.copy()); - } - - if (cost !== -1 && !(modifier.type instanceof RememberMoveModifierType)) { - if (result) { - if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - globalScene.money -= cost; - globalScene.updateMoneyText(); - globalScene.animateMoneyChanged(false); - } - globalScene.playSound("se/buy"); - (globalScene.ui.getHandler() as ModifierSelectUiHandler).updateCostText(); - } else { - globalScene.ui.playError(); - } - } else { - globalScene.ui.clearText(); - globalScene.ui.setMode(UiMode.MESSAGE); - super.end(); - } - } - - // Opens the party menu specifically for fusions - private openFusionMenu( - modifierType: PokemonModifierType, - cost: number, - modifierSelectCallback: ModifierSelectCallback, - ): void { - const party = globalScene.getPlayerParty(); - globalScene.ui.setModeWithoutClear( - UiMode.PARTY, - PartyUiMode.SPLICE, - -1, - (fromSlotIndex: number, spliceSlotIndex: number) => { - if ( - spliceSlotIndex !== undefined && - fromSlotIndex < 6 && - spliceSlotIndex < 6 && - fromSlotIndex !== spliceSlotIndex - ) { - globalScene.ui.setMode(UiMode.MODIFIER_SELECT, this.isPlayer()).then(() => { - const modifier = modifierType.newModifier(party[fromSlotIndex], party[spliceSlotIndex])!; //TODO: is the bang correct? - this.applyModifier(modifier, cost, true); - }); - } else { - this.resetModifierSelect(modifierSelectCallback); - } - }, - modifierType.selectFilter, - ); - } - - // Opens the party menu to apply one of various modifiers - private openModifierMenu( - modifierType: PokemonModifierType, - cost: number, - modifierSelectCallback: ModifierSelectCallback, - ): void { - const party = globalScene.getPlayerParty(); - const pokemonModifierType = modifierType as PokemonModifierType; - const isMoveModifier = modifierType instanceof PokemonMoveModifierType; - const isTmModifier = modifierType instanceof TmModifierType; - const isRememberMoveModifier = modifierType instanceof RememberMoveModifierType; - const isPpRestoreModifier = - modifierType instanceof PokemonPpRestoreModifierType || modifierType instanceof PokemonPpUpModifierType; - const partyUiMode = isMoveModifier - ? PartyUiMode.MOVE_MODIFIER - : isTmModifier - ? PartyUiMode.TM_MODIFIER - : isRememberMoveModifier - ? PartyUiMode.REMEMBER_MOVE_MODIFIER - : PartyUiMode.MODIFIER; - const tmMoveId = isTmModifier ? (modifierType as TmModifierType).moveId : undefined; - globalScene.ui.setModeWithoutClear( - UiMode.PARTY, - partyUiMode, - -1, - (slotIndex: number, option: PartyOption) => { - if (slotIndex < 6) { - globalScene.ui.setMode(UiMode.MODIFIER_SELECT, this.isPlayer()).then(() => { - const modifier = !isMoveModifier - ? !isRememberMoveModifier - ? modifierType.newModifier(party[slotIndex]) - : modifierType.newModifier(party[slotIndex], option as number) - : modifierType.newModifier(party[slotIndex], option - PartyOption.MOVE_1); - this.applyModifier(modifier!, cost, true); // TODO: is the bang correct? - }); - } else { - this.resetModifierSelect(modifierSelectCallback); - } - }, - pokemonModifierType.selectFilter, - modifierType instanceof PokemonMoveModifierType - ? (modifierType as PokemonMoveModifierType).moveSelectFilter - : undefined, - tmMoveId, - isPpRestoreModifier, - ); - } - - // Function that determines how many reward slots are available - private getModifierCount(): number { - const modifierCountHolder = new NumberHolder(3); - globalScene.applyModifiers(ExtraModifierModifier, true, modifierCountHolder); - globalScene.applyModifiers(TempExtraModifierModifier, true, modifierCountHolder); - - // If custom modifiers are specified, overrides default item count - if (this.customModifierSettings) { - const newItemCount = - (this.customModifierSettings.guaranteedModifierTiers?.length ?? 0) + - (this.customModifierSettings.guaranteedModifierTypeOptions?.length ?? 0) + - (this.customModifierSettings.guaranteedModifierTypeFuncs?.length ?? 0); - if (this.customModifierSettings.fillRemaining) { - const originalCount = modifierCountHolder.value; - modifierCountHolder.value = originalCount > newItemCount ? originalCount : newItemCount; - } else { - modifierCountHolder.value = newItemCount; - } - } - - return modifierCountHolder.value; - } - - // Function that resets the reward selection screen, - // e.g. after pressing cancel in the party ui or while learning a move - private resetModifierSelect(modifierSelectCallback: ModifierSelectCallback) { - globalScene.ui.setMode( - UiMode.MODIFIER_SELECT, - this.isPlayer(), - this.typeOptions, - modifierSelectCallback, - this.getRerollCost(globalScene.lockModifierTiers), - ); - } - - updateSeed(): void { - globalScene.resetSeed(); - } - - isPlayer(): boolean { - return true; - } - - getRerollCost(lockRarities: boolean): number { - let baseValue = 0; - if (Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - return baseValue; - } - if (lockRarities) { - const tierValues = [50, 125, 300, 750, 2000]; - for (const opt of this.typeOptions) { - baseValue += tierValues[opt.type.tier ?? 0]; - } - } else { - baseValue = 250; - } - - let multiplier = 1; - if (!isNullOrUndefined(this.customModifierSettings?.rerollMultiplier)) { - if (this.customModifierSettings.rerollMultiplier < 0) { - // Completely overrides reroll cost to -1 and early exits - return -1; - } - - // Otherwise, continue with custom multiplier - multiplier = this.customModifierSettings.rerollMultiplier; - } - - const baseMultiplier = Math.min( - Math.ceil(globalScene.currentBattle.waveIndex / 10) * baseValue * 2 ** this.rerollCount * multiplier, - Number.MAX_SAFE_INTEGER, - ); - - // Apply Black Sludge to reroll cost - const modifiedRerollCost = new NumberHolder(baseMultiplier); - globalScene.applyModifier(HealShopCostModifier, true, modifiedRerollCost); - return modifiedRerollCost.value; - } - - getPoolType(): ModifierPoolType { - return ModifierPoolType.PLAYER; - } - - getModifierTypeOptions(modifierCount: number): ModifierTypeOption[] { - return getPlayerModifierTypeOptions( - modifierCount, - globalScene.getPlayerParty(), - globalScene.lockModifierTiers ? this.modifierTiers : undefined, - this.customModifierSettings, - ); - } - - copy(): SelectModifierPhase { - return globalScene.phaseManager.create( - "SelectModifierPhase", - this.rerollCount, - this.modifierTiers, - { - guaranteedModifierTypeOptions: this.typeOptions, - rerollMultiplier: this.customModifierSettings?.rerollMultiplier, - allowLuckUpgrades: false, - }, - true, - ); - } - - addModifier(modifier: Modifier): boolean { - return globalScene.addModifier(modifier, false, true); - } -} diff --git a/src/phases/select-reward-phase.ts b/src/phases/select-reward-phase.ts new file mode 100644 index 00000000000..e552b209666 --- /dev/null +++ b/src/phases/select-reward-phase.ts @@ -0,0 +1,470 @@ +import { globalScene } from "#app/global-scene"; +import Overrides from "#app/overrides"; +import type { MoveId } from "#enums/move-id"; +import { RewardPoolType } from "#enums/reward-pool-type"; +import type { RarityTier } from "#enums/reward-tier"; +import { UiMode } from "#enums/ui-mode"; +import type { + PokemonMoveRecallRewardParams, + PokemonMoveRewardParams, + PokemonRewardParams, + Reward, + RewardOption, +} from "#items/reward"; +import { FusePokemonReward, type PokemonMoveReward, PokemonReward, RememberMoveReward, TmReward } from "#items/reward"; +import { + type CustomRewardSettings, + generatePlayerRewardOptions, + generateRewardPoolWeights, + getRewardPoolForType, +} from "#items/reward-pool-utils"; +import { getPlayerShopRewardOptionsForWave, isMoveReward, isRememberMoveReward, isTmReward } from "#items/reward-utils"; +import { TrainerItemEffect } from "#items/trainer-item"; +import { BattlePhase } from "#phases/battle-phase"; +import { PartyOption, PartyUiHandler, PartyUiMode, type PokemonMoveSelectFilter } from "#ui/party-ui-handler"; +import { type RewardSelectUiHandler, SHOP_OPTIONS_ROW_LIMIT } from "#ui/reward-select-ui-handler"; +import { isNullOrUndefined, NumberHolder } from "#utils/common"; +import i18next from "i18next"; + +export type RewardSelectCallback = (rowCursor: number, cursor: number) => boolean; + +export class SelectRewardPhase extends BattlePhase { + public readonly phaseName = "SelectRewardPhase"; + private rerollCount: number; + private rarityTiers?: RarityTier[]; + private customRewardSettings?: CustomRewardSettings; + private isCopy: boolean; + + private typeOptions: RewardOption[]; + + constructor( + rerollCount = 0, + rarityTiers?: RarityTier[], + customRewardSettings?: CustomRewardSettings, + isCopy = false, + ) { + super(); + + this.rerollCount = rerollCount; + this.rarityTiers = rarityTiers; + this.customRewardSettings = customRewardSettings; + this.isCopy = isCopy; + } + + start() { + super.start(); + + if (!this.isPlayer()) { + return false; + } + + if (!this.rerollCount && !this.isCopy) { + this.updateSeed(); + } else if (this.rerollCount) { + globalScene.reroll = false; + } + + const party = globalScene.getPlayerParty(); + if (!this.isCopy) { + generateRewardPoolWeights(getRewardPoolForType(this.getPoolType()), party, this.rerollCount); + } + const rewardCount = this.getRewardCount(); + + this.typeOptions = this.getRewardOptions(rewardCount); + + const rewardSelectCallback = (rowCursor: number, cursor: number) => { + if (rowCursor < 0 || cursor < 0) { + // Attempt to skip the item pickup + globalScene.ui.showText(i18next.t("battle:skipItemQuestion"), null, () => { + globalScene.ui.setOverlayMode( + UiMode.CONFIRM, + () => { + globalScene.ui.revertMode(); + globalScene.ui.setMode(UiMode.MESSAGE); + super.end(); + }, + () => this.resetRewardSelect(rewardSelectCallback), + ); + }); + return false; + } + + switch (rowCursor) { + // Execute one of the options from the bottom row + case 0: + switch (cursor) { + case 0: + return this.rerollRewards(); + case 1: + return this.openItemTransferScreen(rewardSelectCallback); + // Check the party, pass a callback to restore the reward select screen. + case 2: + globalScene.ui.setModeWithoutClear(UiMode.PARTY, PartyUiMode.CHECK, -1, () => { + this.resetRewardSelect(rewardSelectCallback); + }); + return true; + case 3: + return this.toggleRerollLock(); + default: + return false; + } + // Pick an option from the allRewards + case 1: + return this.selectRewardOption(cursor, rewardSelectCallback); + // Pick an option from the shop + default: { + return this.selectShopOption(rowCursor, cursor, rewardSelectCallback); + } + } + }; + + this.resetRewardSelect(rewardSelectCallback); + } + + // Pick a reward from among the allRewards and apply it + private selectRewardOption(cursor: number, rewardSelectCallback: RewardSelectCallback): boolean { + if (this.typeOptions.length === 0) { + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.MESSAGE); + super.end(); + return true; + } + const reward = this.typeOptions[cursor].type; + return this.applyReward(reward, -1, rewardSelectCallback); + } + + // Pick a reward from the shop and apply it + private selectShopOption(rowCursor: number, cursor: number, rewardSelectCallback: RewardSelectCallback): boolean { + const shopOptions = getPlayerShopRewardOptionsForWave( + globalScene.currentBattle.waveIndex, + globalScene.getWaveMoneyAmount(1), + ); + const shopOption = + shopOptions[ + rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT + ]; + const reward = shopOption.type; + // Apply Black Sludge to healing item cost + const healingItemCost = new NumberHolder(shopOption.cost); + globalScene.applyPlayerItems(TrainerItemEffect.HEAL_SHOP_COST, { numberHolder: healingItemCost }); + const cost = healingItemCost.value; + + if (globalScene.money < cost && !Overrides.WAIVE_ROLL_FEE_OVERRIDE) { + globalScene.ui.playError(); + return false; + } + + return this.applyReward(reward, cost, rewardSelectCallback); + } + + // Apply a chosen reward: do an effect or open the party menu + private applyReward(reward: Reward, cost: number, rewardSelectCallback: RewardSelectCallback): boolean { + if (reward instanceof PokemonReward) { + if (reward instanceof FusePokemonReward) { + this.openFusionMenu(reward, cost, rewardSelectCallback); + } else { + this.openPokemonRewardMenu(reward, cost, rewardSelectCallback); + } + } else { + globalScene.applyReward(reward, {}, true); + globalScene.updateItems(true); + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.MESSAGE); + super.end(); + } + return cost === -1; + } + + // Reroll allRewards + private rerollRewards() { + const rerollCost = this.getRerollCost(globalScene.lockRarityTiers); + if (rerollCost < 0 || globalScene.money < rerollCost) { + globalScene.ui.playError(); + return false; + } + globalScene.reroll = true; + globalScene.phaseManager.unshiftNew( + "SelectRewardPhase", + this.rerollCount + 1, + this.typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as RarityTier[], + ); + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end()); + if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { + globalScene.money -= rerollCost; + globalScene.updateMoneyText(); + globalScene.animateMoneyChanged(false); + } + globalScene.playSound("se/buy"); + return true; + } + + // Transfer rewards among party pokemon + private openItemTransferScreen(rewardSelectCallback: RewardSelectCallback) { + const party = globalScene.getPlayerParty(); + globalScene.ui.setModeWithoutClear( + UiMode.PARTY, + PartyUiMode.ITEM_TRANSFER, + -1, + (fromSlotIndex: number, itemIndex: number, itemQuantity: number, toSlotIndex: number) => { + if ( + toSlotIndex !== undefined && + fromSlotIndex < 6 && + toSlotIndex < 6 && + fromSlotIndex !== toSlotIndex && + itemIndex > -1 + ) { + const items = party[fromSlotIndex].heldItemManager.getTransferableHeldItems(); + const item = items[itemIndex]; + globalScene.tryTransferHeldItem( + item, + party[fromSlotIndex], + party[toSlotIndex], + true, + itemQuantity, + undefined, + false, + ); + } else { + this.resetRewardSelect(rewardSelectCallback); + } + }, + PartyUiHandler.FilterItemMaxStacks, + ); + return true; + } + + // Toggle reroll lock + private toggleRerollLock() { + const rerollCost = this.getRerollCost(globalScene.lockRarityTiers); + if (rerollCost < 0) { + // Reroll lock button is also disabled when reroll is disabled + globalScene.ui.playError(); + return false; + } + globalScene.lockRarityTiers = !globalScene.lockRarityTiers; + const uiHandler = globalScene.ui.getHandler() as RewardSelectUiHandler; + uiHandler.setRerollCost(this.getRerollCost(globalScene.lockRarityTiers)); + uiHandler.updateLockRaritiesText(); + uiHandler.updateRerollCostText(); + return false; + } + + // Opens the party menu specifically for fusions + private openFusionMenu(reward: FusePokemonReward, _cost: number, rewardSelectCallback: RewardSelectCallback): void { + const party = globalScene.getPlayerParty(); + globalScene.ui.setModeWithoutClear( + UiMode.PARTY, + PartyUiMode.SPLICE, + -1, + (fromSlotIndex: number, spliceSlotIndex: number) => { + if ( + spliceSlotIndex !== undefined && + fromSlotIndex < 6 && + spliceSlotIndex < 6 && + fromSlotIndex !== spliceSlotIndex + ) { + globalScene.ui.setMode(UiMode.REWARD_SELECT, this.isPlayer()).then(() => { + reward.apply({ pokemon: party[fromSlotIndex], pokemon2: party[spliceSlotIndex] }); + }); + } else { + this.resetRewardSelect(rewardSelectCallback); + } + }, + reward.selectFilter, + ); + } + + // Opens the party menu to apply one of various Pokemon rewards. We pass the reward's filter to decide which Pokemon can be selected. + // For MoveReward (e.g. PP UP or Ether) we also pass a filter to decide which moves can be selected. + private openPokemonRewardMenu(reward: PokemonReward, cost: number, rewardSelectCallback: RewardSelectCallback): void { + const party = globalScene.getPlayerParty(); + + let partyUiMode = PartyUiMode.REWARD; + let moveSelectFilter: PokemonMoveSelectFilter | undefined; + let tmMoveId: MoveId | undefined; + let isMove = false; + let getParams = (slotIndex: number, _option: PartyOption) => { + return { pokemon: party[slotIndex] } as PokemonRewardParams; + }; + + if (isMoveReward(reward)) { + partyUiMode = PartyUiMode.MOVE_REWARD; + moveSelectFilter = (reward as PokemonMoveReward).moveSelectFilter; + isMove = true; + getParams = (slotIndex: number, option: PartyOption) => { + return { pokemon: party[slotIndex], moveIndex: option - PartyOption.MOVE_1 } as PokemonMoveRewardParams; + }; + } + if (isRememberMoveReward(reward)) { + partyUiMode = PartyUiMode.REMEMBER_MOVE_REWARD; + getParams = (slotIndex: number, option: PartyOption) => { + return { pokemon: party[slotIndex], moveIndex: option, cost: cost } as PokemonMoveRecallRewardParams; + }; + } + if (isTmReward(reward)) { + partyUiMode = PartyUiMode.TM_REWARD; + tmMoveId = reward.moveId; + } + + globalScene.ui.setModeWithoutClear( + UiMode.PARTY, + partyUiMode, + -1, + (slotIndex: number, option: PartyOption) => { + if (slotIndex < 6) { + globalScene.ui.setMode(UiMode.REWARD_SELECT, this.isPlayer()).then(() => { + const params = getParams(slotIndex, option); + const result = globalScene.applyReward(reward, params, true); + this.postApplyPokemonReward(reward, result, cost); + }); + } else { + this.resetRewardSelect(rewardSelectCallback); + } + }, + reward.selectFilter, + moveSelectFilter, + tmMoveId, + isMove, + ); + } + + // TODO: Rework this to work properly with rewards + /** + * Apply the effects of the chosen reward + * @param reward - The reward to apply + * @param cost - The cost of the reward if it was purchased, or -1 if selected as the reward reward + * @param playSound - Whether the 'obtain reward' sound should be played when adding the reward. + */ + private postApplyPokemonReward(reward: Reward, result = false, cost = -1): void { + // Queue a copy of this phase when applying a TM or Memory Mushroom. + // If the player selects either of these, then escapes out of consuming them, + // they are returned to a shop in the same state. + if (reward instanceof RememberMoveReward || reward instanceof TmReward) { + globalScene.phaseManager.unshiftPhase(this.copy()); + } + + if (cost !== -1 && !(reward instanceof RememberMoveReward)) { + if (result) { + if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { + globalScene.money -= cost; + globalScene.updateMoneyText(); + globalScene.animateMoneyChanged(false); + } + globalScene.playSound("se/buy"); + (globalScene.ui.getHandler() as RewardSelectUiHandler).updateCostText(); + } else { + globalScene.ui.playError(); + } + } else { + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.MESSAGE); + super.end(); + } + } + + // Function that determines how many reward slots are available + private getRewardCount(): number { + const rewardCountHolder = new NumberHolder(3); + globalScene.applyPlayerItems(TrainerItemEffect.EXTRA_REWARD, { numberHolder: rewardCountHolder }); + + // If custom rewards are specified, overrides default item count + if (this.customRewardSettings) { + const newItemCount = + (this.customRewardSettings.guaranteedRarityTiers?.length ?? 0) + + (this.customRewardSettings.guaranteedRewardOptions?.length ?? 0) + + (this.customRewardSettings.guaranteedRewardSpecs?.length ?? 0); + if (this.customRewardSettings.fillRemaining) { + const originalCount = rewardCountHolder.value; + rewardCountHolder.value = originalCount > newItemCount ? originalCount : newItemCount; + } else { + rewardCountHolder.value = newItemCount; + } + } + + return rewardCountHolder.value; + } + + // Function that resets the reward selection screen, + // e.g. after pressing cancel in the party ui or while learning a move + private resetRewardSelect(rewardSelectCallback: RewardSelectCallback) { + globalScene.ui.setMode( + UiMode.REWARD_SELECT, + this.isPlayer(), + this.typeOptions, + rewardSelectCallback, + this.getRerollCost(globalScene.lockRarityTiers), + ); + } + + updateSeed(): void { + globalScene.resetSeed(); + } + + isPlayer(): boolean { + return true; + } + + getRerollCost(lockRarities: boolean): number { + let baseValue = 0; + if (Overrides.WAIVE_ROLL_FEE_OVERRIDE) { + return baseValue; + } + if (lockRarities) { + const tierValues = [50, 125, 300, 750, 2000]; + for (const opt of this.typeOptions) { + baseValue += tierValues[opt.type.tier ?? 0]; + } + } else { + baseValue = 250; + } + + let multiplier = 1; + if (!isNullOrUndefined(this.customRewardSettings?.rerollMultiplier)) { + if (this.customRewardSettings.rerollMultiplier < 0) { + // Completely overrides reroll cost to -1 and early exits + return -1; + } + + // Otherwise, continue with custom multiplier + multiplier = this.customRewardSettings.rerollMultiplier; + } + + const baseMultiplier = Math.min( + Math.ceil(globalScene.currentBattle.waveIndex / 10) * baseValue * 2 ** this.rerollCount * multiplier, + Number.MAX_SAFE_INTEGER, + ); + + // Apply Black Sludge to reroll cost + const modifiedRerollCost = new NumberHolder(baseMultiplier); + globalScene.applyPlayerItems(TrainerItemEffect.HEAL_SHOP_COST, { numberHolder: modifiedRerollCost }); + return modifiedRerollCost.value; + } + + getPoolType(): RewardPoolType { + return RewardPoolType.PLAYER; + } + + getRewardOptions(rewardCount: number): RewardOption[] { + return generatePlayerRewardOptions( + rewardCount, + globalScene.getPlayerParty(), + globalScene.lockRarityTiers ? this.rarityTiers : undefined, + this.customRewardSettings, + ); + } + + copy(): SelectRewardPhase { + return globalScene.phaseManager.create( + "SelectRewardPhase", + this.rerollCount, + this.rarityTiers, + { + guaranteedRewardOptions: this.typeOptions, + rerollMultiplier: this.customRewardSettings?.rerollMultiplier, + allowLuckUpgrades: false, + }, + true, + ); + } +} diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index d6bd252c77d..61f81f1e468 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -7,7 +7,7 @@ import { Gender } from "#data/gender"; import { ChallengeType } from "#enums/challenge-type"; import type { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { overrideHeldItems, overrideModifiers } from "#modifiers/modifier"; +import { overrideHeldItems, overrideTrainerItems } from "#items/item-overrides"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; import type { Starter } from "#ui/starter-select-ui-handler"; import { isNullOrUndefined } from "#utils/common"; @@ -107,7 +107,7 @@ export class SelectStarterPhase extends Phase { } loadPokemonAssets.push(starterPokemon.loadAssets()); }); - overrideModifiers(); + overrideTrainerItems(); overrideHeldItems(party[0]); Promise.all(loadPokemonAssets).then(() => { SoundFade.fadeOut(globalScene, globalScene.sound.get("menu"), 500, true); diff --git a/src/phases/show-party-exp-bar-phase.ts b/src/phases/show-party-exp-bar-phase.ts index f22be5f63e3..4599c4b37cc 100644 --- a/src/phases/show-party-exp-bar-phase.ts +++ b/src/phases/show-party-exp-bar-phase.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { ExpNotification } from "#enums/exp-notification"; -import { ExpBoosterModifier } from "#modifiers/modifier"; +import { TrainerItemEffect } from "#items/trainer-item"; import { PlayerPartyMemberPokemonPhase } from "#phases/player-party-member-pokemon-phase"; import { NumberHolder } from "#utils/common"; @@ -20,7 +20,7 @@ export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { const pokemon = this.getPokemon(); const exp = new NumberHolder(this.expValue); - globalScene.applyModifiers(ExpBoosterModifier, true, exp); + globalScene.applyPlayerItems(TrainerItemEffect.EXP_BOOSTER, { numberHolder: exp }); exp.value = Math.floor(exp.value); const lastLevel = pokemon.level; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 140ef841929..8156149628a 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -8,9 +8,10 @@ import { OctolockTag } from "#data/battler-tags"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerIndex } from "#enums/battler-index"; +import { HeldItemEffect } from "#enums/held-item-effect"; import { type BattleStat, getStatKey, getStatStageChangeDescriptionKey, Stat } from "#enums/stat"; import type { Pokemon } from "#field/pokemon"; -import { ResetNegativeStatStageModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import { PokemonPhase } from "#phases/pokemon-phase"; import type { ConditionalUserFieldProtectStatAbAttrParams, PreStatStageChangeAbAttrParams } from "#types/ability-types"; import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#utils/common"; @@ -232,16 +233,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(HeldItemEffect.RESET_NEGATIVE_STAT_STAGE, { pokemon: pokemon, isPlayer: this.player }); } pokemon.updateInfo(); diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index e4c8aa9af7a..24e329ca99d 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -177,7 +177,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); } addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.getPokeball(true)); - globalScene.updateModifiers(this.player); + globalScene.updateItems(this.player); globalScene.updateFieldScale(); pokemon.showInfo(); pokemon.playAnim(); @@ -235,7 +235,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { } globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); } - globalScene.updateModifiers(this.player); + globalScene.updateItems(this.player); globalScene.updateFieldScale(); pokemon.showInfo(); pokemon.playAnim(); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index b7460e77569..cc6b699aa6e 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -6,10 +6,10 @@ import { allMoves } from "#data/data-lists"; import { SpeciesFormChangeActiveTrigger } from "#data/form-change-triggers"; import { getPokeballTintColor } from "#data/pokeball"; import { Command } from "#enums/command"; +import { HeldItemId } from "#enums/held-item-id"; import { SwitchType } from "#enums/switch-type"; import { TrainerSlot } from "#enums/trainer-slot"; import type { Pokemon } from "#field/pokemon"; -import { SwitchEffectTransferModifier } from "#modifiers/modifier"; import { SummonPhase } from "#phases/summon-phase"; import i18next from "i18next"; @@ -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 6f0493f707d..1e56965dc0e 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -4,16 +4,14 @@ import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; import { Phase } from "#app/phase"; import { fetchDailyRunSeed, getDailyRunStarters } from "#data/daily-run"; -import { modifierTypes } from "#data/data-lists"; import { Gender } from "#data/gender"; import { BattleType } from "#enums/battle-type"; import { GameModes } from "#enums/game-modes"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; import { Unlockables } from "#enums/unlockables"; import { getBiomeKey } from "#field/arena"; -import type { Modifier } from "#modifiers/modifier"; -import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; +import { assignDailyRunStarterHeldItems } from "#items/held-item-pool"; import type { SessionSaveData } from "#system/game-data"; import { vouchers } from "#system/voucher"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; @@ -239,24 +237,13 @@ export class TitlePhase extends Phase { loadPokemonAssets.push(starterPokemon.loadAssets()); } - regenerateModifierPoolThresholds(party, ModifierPoolType.DAILY_STARTER); + globalScene.trainerItems.add(TrainerItemId.EXP_SHARE, 3); + globalScene.trainerItems.add(TrainerItemId.GOLDEN_EXP_CHARM, 3); + globalScene.trainerItems.add(TrainerItemId.MAP); - const modifiers: Modifier[] = Array(3) - .fill(null) - .map(() => modifierTypes.EXP_SHARE().withIdFromFunc(modifierTypes.EXP_SHARE).newModifier()) - .concat( - Array(3) - .fill(null) - .map(() => modifierTypes.GOLDEN_EXP_CHARM().withIdFromFunc(modifierTypes.GOLDEN_EXP_CHARM).newModifier()), - ) - .concat([modifierTypes.MAP().withIdFromFunc(modifierTypes.MAP).newModifier()]) - .concat(getDailyRunStarterModifiers(party)) - .filter(m => m !== null); + assignDailyRunStarterHeldItems(party); - for (const m of modifiers) { - globalScene.addModifier(m, true, false, false, true); - } - globalScene.updateModifiers(true, true); + globalScene.updateItems(true); Promise.all(loadPokemonAssets).then(() => { globalScene.time.delayedCall(500, () => globalScene.playBgm()); diff --git a/src/phases/trainer-item-reward-phase.ts b/src/phases/trainer-item-reward-phase.ts new file mode 100644 index 00000000000..033a022958f --- /dev/null +++ b/src/phases/trainer-item-reward-phase.ts @@ -0,0 +1,40 @@ +import { globalScene } from "#app/global-scene"; +import type { Reward } from "#items/reward"; +import { BattlePhase } from "#phases/battle-phase"; +import type { RewardFunc } from "#types/rewards"; +import i18next from "i18next"; + +export class RewardPhase extends BattlePhase { + // RibbonRewardPhase extends RewardPhase and to make typescript happy + // we need to use a union type here + public readonly phaseName: "RewardPhase" | "RibbonRewardPhase" | "GameOverRewardPhase" = "RewardPhase"; + protected reward: Reward; + + constructor(rewardFunc: RewardFunc) { + super(); + + this.reward = rewardFunc(); + } + + start() { + super.start(); + + this.doReward().then(() => this.end()); + } + + doReward(): Promise { + return new Promise(resolve => { + globalScene.applyReward(this.reward); + globalScene.playSound("item_fanfare"); + globalScene.ui.showText( + i18next.t("battle:rewardGain", { + modifierName: this.reward.name, + }), + null, + () => resolve(), + null, + true, + ); + }); + } +} diff --git a/src/phases/trainer-victory-phase.ts b/src/phases/trainer-victory-phase.ts index 6f92dbe496d..df9026fc536 100644 --- a/src/phases/trainer-victory-phase.ts +++ b/src/phases/trainer-victory-phase.ts @@ -1,6 +1,6 @@ import { timedEventManager } from "#app/global-event-manager"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; +import { allRewards } from "#data/data-lists"; import { getCharVariantFromDialogue } from "#data/dialogue"; import { BiomeId } from "#enums/biome-id"; import { TrainerSlot } from "#enums/trainer-slot"; @@ -20,9 +20,9 @@ export class TrainerVictoryPhase extends BattlePhase { globalScene.phaseManager.unshiftNew("MoneyRewardPhase", globalScene.currentBattle.trainer?.config.moneyMultiplier!); // TODO: is this bang correct? - const modifierRewardFuncs = globalScene.currentBattle.trainer?.config.modifierRewardFuncs!; // TODO: is this bang correct? - for (const modifierRewardFunc of modifierRewardFuncs) { - globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierRewardFunc); + const rewardFuncs = globalScene.currentBattle.trainer?.config.rewardFuncs!; // TODO: is this bang correct? + for (const rewardFunc of rewardFuncs) { + globalScene.phaseManager.unshiftNew("RewardPhase", rewardFunc); } const trainerType = globalScene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct? @@ -34,18 +34,15 @@ export class TrainerVictoryPhase extends BattlePhase { ) { if (timedEventManager.getUpgradeUnlockedVouchers()) { globalScene.phaseManager.unshiftNew( - "ModifierRewardPhase", - [ - modifierTypes.VOUCHER_PLUS, - modifierTypes.VOUCHER_PLUS, - modifierTypes.VOUCHER_PLUS, - modifierTypes.VOUCHER_PREMIUM, - ][vouchers[TrainerType[trainerType]].voucherType], + "RewardPhase", + [allRewards.VOUCHER_PLUS, allRewards.VOUCHER_PLUS, allRewards.VOUCHER_PLUS, allRewards.VOUCHER_PREMIUM][ + vouchers[TrainerType[trainerType]].voucherType + ], ); } else { globalScene.phaseManager.unshiftNew( - "ModifierRewardPhase", - [modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][ + "RewardPhase", + [allRewards.VOUCHER, allRewards.VOUCHER, allRewards.VOUCHER_PLUS, allRewards.VOUCHER_PREMIUM][ vouchers[TrainerType[trainerType]].voucherType ], ); diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 463f26e73a2..2a42c560433 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -3,16 +3,12 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { TerrainType } from "#data/terrain"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; +import { HeldItemEffect } from "#enums/held-item-effect"; import { WeatherType } from "#enums/weather-type"; import { TurnEndEvent } from "#events/battle-scene"; import type { Pokemon } from "#field/pokemon"; -import { - EnemyStatusEffectHealChanceModifier, - EnemyTurnHealModifier, - TurnHealModifier, - TurnHeldItemTransferModifier, - TurnStatusEffectModifier, -} from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; +import { TrainerItemEffect } from "#items/trainer-item"; import { FieldPhase } from "#phases/field-phase"; import i18next from "i18next"; @@ -32,7 +28,7 @@ export class TurnEndPhase extends FieldPhase { if (!pokemon.switchOutStatus) { pokemon.lapseTags(BattlerTagLapseType.TURN_END); - globalScene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); + applyHeldItems(HeldItemEffect.TURN_END_HEAL, { pokemon: pokemon }); if (globalScene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { globalScene.phaseManager.unshiftNew( @@ -47,15 +43,16 @@ export class TurnEndPhase extends FieldPhase { } if (!pokemon.isPlayer()) { - globalScene.applyModifiers(EnemyTurnHealModifier, false, pokemon); - globalScene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon); + globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_HEAL, { pokemon: pokemon }); + globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_STATUS_HEAL_CHANCE, { pokemon: pokemon }); } applyAbAttrs("PostTurnAbAttr", { pokemon }); } - globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); - globalScene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); + applyHeldItems(HeldItemEffect.TURN_END_STATUS, { pokemon: pokemon }); + + applyHeldItems(HeldItemEffect.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 9c53a333ed0..70743b8b6ae 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -4,10 +4,11 @@ import { TrickRoomTag } from "#data/arena-tag"; import { allMoves } from "#data/data-lists"; import { BattlerIndex } from "#enums/battler-index"; import { Command } from "#enums/command"; +import { HeldItemEffect } from "#enums/held-item-effect"; import { Stat } from "#enums/stat"; import { SwitchType } from "#enums/switch-type"; import type { Pokemon } from "#field/pokemon"; -import { BypassSpeedChanceModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import { PokemonMove } from "#moves/pokemon-move"; import { FieldPhase } from "#phases/field-phase"; import { BooleanHolder, randSeedShuffle } from "#utils/common"; @@ -72,7 +73,7 @@ export class TurnStartPhase extends FieldPhase { canCheckHeldItems: canCheckHeldItems, }); if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); + applyHeldItems(HeldItemEffect.BYPASS_SPEED_CHANCE, { pokemon: p, doBypassSpeed: bypassSpeed }); } battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; }); diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 4b1a79d7443..8fdf8d52bc3 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -1,10 +1,10 @@ import { timedEventManager } from "#app/global-event-manager"; import { globalScene } from "#app/global-scene"; -import { modifierTypes } from "#data/data-lists"; +import { allRewards } from "#data/data-lists"; import { BattleType } from "#enums/battle-type"; import type { BattlerIndex } from "#enums/battler-index"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; -import type { CustomModifierSettings } from "#modifiers/modifier-type"; +import type { CustomRewardSettings } from "#items/reward-pool-utils"; import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils"; import { PokemonPhase } from "#phases/pokemon-phase"; @@ -55,54 +55,54 @@ export class VictoryPhase extends PokemonPhase { // Get event modifiers for this wave timedEventManager .getFixedBattleEventRewards(globalScene.currentBattle.waveIndex) - .map(r => globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes[r])); + .map(r => globalScene.phaseManager.pushNew("RewardPhase", allRewards[r])); break; case ClassicFixedBossWaves.EVIL_BOSS_2: - // Should get Lock Capsule on 165 before shop phase so it can be used in the rewards shop - globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.LOCK_CAPSULE); + // Should get Lock Capsule on 165 before shop phase so it can be used in the allRewards shop + globalScene.phaseManager.pushNew("RewardPhase", allRewards.LOCK_CAPSULE); break; } } if (globalScene.currentBattle.waveIndex % 10) { globalScene.phaseManager.pushNew( - "SelectModifierPhase", + "SelectRewardPhase", undefined, undefined, - this.getFixedBattleCustomModifiers(), + this.getFixedBattleCustomRewards(), ); } else if (globalScene.gameMode.isDaily) { - globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_CHARM); + globalScene.phaseManager.pushNew("RewardPhase", allRewards.EXP_CHARM); if ( globalScene.currentBattle.waveIndex > 10 && !globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex) ) { - globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL); + globalScene.phaseManager.pushNew("RewardPhase", allRewards.GOLDEN_POKEBALL); } } else { const superExpWave = !globalScene.gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10; if (globalScene.gameMode.isEndless && globalScene.currentBattle.waveIndex === 10) { - globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_SHARE); + globalScene.phaseManager.pushNew("RewardPhase", allRewards.EXP_SHARE); } if ( globalScene.currentBattle.waveIndex <= 750 && (globalScene.currentBattle.waveIndex <= 500 || globalScene.currentBattle.waveIndex % 30 === superExpWave) ) { globalScene.phaseManager.pushNew( - "ModifierRewardPhase", + "RewardPhase", globalScene.currentBattle.waveIndex % 30 !== superExpWave || globalScene.currentBattle.waveIndex > 250 - ? modifierTypes.EXP_CHARM - : modifierTypes.SUPER_EXP_CHARM, + ? allRewards.EXP_CHARM + : allRewards.SUPER_EXP_CHARM, ); } if (globalScene.currentBattle.waveIndex <= 150 && !(globalScene.currentBattle.waveIndex % 50)) { - globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL); + globalScene.phaseManager.pushNew("RewardPhase", allRewards.GOLDEN_POKEBALL); } if (globalScene.gameMode.isEndless && !(globalScene.currentBattle.waveIndex % 50)) { globalScene.phaseManager.pushNew( - "ModifierRewardPhase", - !(globalScene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS, + "RewardPhase", + !(globalScene.currentBattle.waveIndex % 250) ? allRewards.VOUCHER_PREMIUM : allRewards.VOUCHER_PLUS, ); - globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase"); + globalScene.phaseManager.pushNew("AddEnemyTokenPhase"); } } @@ -123,14 +123,14 @@ export class VictoryPhase extends PokemonPhase { } /** - * If this wave is a fixed battle with special custom modifier rewards, - * will pass those settings to the upcoming {@linkcode SelectModifierPhase}`. + * If this wave is a fixed battle with special custom modifier allRewards, + * will pass those settings to the upcoming {@linkcode SelectRewardPhase}`. */ - getFixedBattleCustomModifiers(): CustomModifierSettings | undefined { + getFixedBattleCustomRewards(): CustomRewardSettings | undefined { const gameMode = globalScene.gameMode; const waveIndex = globalScene.currentBattle.waveIndex; if (gameMode.isFixedBattle(waveIndex)) { - return gameMode.getFixedBattle(waveIndex).customModifierRewardSettings; + return gameMode.getFixedBattle(waveIndex).customRewardSettings; } return undefined; diff --git a/src/system/achv.ts b/src/system/achv.ts index 69eade02e35..8c6210ae326 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -9,13 +9,13 @@ import { SingleTypeChallenge, } from "#data/challenge"; import { Challenges } from "#enums/challenges"; +import { HeldItemId } from "#enums/held-item-id"; import { PlayerGender } from "#enums/player-gender"; import { getShortenedStatKey, Stat } from "#enums/stat"; -import { TurnHeldItemTransferModifier } from "#modifiers/modifier"; +import type { Pokemon } from "#field/pokemon"; import type { ConditionFn } from "#types/common"; import { NumberHolder } from "#utils/common"; import i18next from "i18next"; -import type { Modifier } from "typescript"; export enum AchvTier { COMMON, @@ -176,16 +176,16 @@ export class LevelAchv extends Achv { } } -export class ModifierAchv extends Achv { +export class HeldItemAchv extends Achv { constructor( localizationKey: string, name: string, description: string, iconImage: string, score: number, - modifierFunc: (modifier: Modifier) => boolean, + pokemonFunc: (pokemon: Pokemon) => boolean, ) { - super(localizationKey, name, description, iconImage, score, (args: any[]) => modifierFunc(args[0] as Modifier)); + super(localizationKey, name, description, iconImage, score, (args: any[]) => pokemonFunc(args[0] as Pokemon)); } } @@ -490,13 +490,13 @@ export const achvs = { 25, ).setSecret(true), SPLICE: new Achv("SPLICE", "", "SPLICE.description", "dna_splicers", 10), - MINI_BLACK_HOLE: new ModifierAchv( + MINI_BLACK_HOLE: new HeldItemAchv( "MINI_BLACK_HOLE", "", "MINI_BLACK_HOLE.description", "mini_black_hole", 25, - modifier => modifier instanceof TurnHeldItemTransferModifier, + pokemon => pokemon.heldItemManager.hasItem(HeldItemId.MINI_BLACK_HOLE), ).setSecret(), CATCH_MYTHICAL: new Achv("CATCH_MYTHICAL", "", "CATCH_MYTHICAL.description", "strange_ball", 50).setSecret(), CATCH_SUB_LEGENDARY: new Achv("CATCH_SUB_LEGENDARY", "", "CATCH_SUB_LEGENDARY.description", "rb", 75).setSecret(), diff --git a/src/system/game-data.ts b/src/system/game-data.ts index d899afa19ef..4f6ed1257a8 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -31,15 +31,13 @@ import { Nature } from "#enums/nature"; import { PlayerGender } from "#enums/player-gender"; import type { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; import { TrainerVariant } from "#enums/trainer-variant"; import { UiMode } from "#enums/ui-mode"; import { Unlockables } from "#enums/unlockables"; import { WeatherType } from "#enums/weather-type"; import { TagAddedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#events/arena"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; -// biome-ignore lint/performance/noNamespaceImport: Something weird is going on here and I don't want to touch it -import * as Modifier from "#modifiers/modifier"; +import type { TrainerItemConfiguration, TrainerItemSaveData } from "#items/trainer-item-data-types"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import type { Variant } from "#sprites/variant"; import { achvs } from "#system/achv"; @@ -47,7 +45,6 @@ import { ArenaData, type SerializedArenaData } from "#system/arena-data"; import { ChallengeData } from "#system/challenge-data"; import { EggData } from "#system/egg-data"; import { GameStats } from "#system/game-stats"; -import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; import { resetSettings, SettingKeys, setSetting } from "#system/settings"; import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad"; @@ -117,8 +114,8 @@ export interface SessionSaveData { gameMode: GameModes; party: PokemonData[]; enemyParty: PokemonData[]; - modifiers: PersistentModifierData[]; - enemyModifiers: PersistentModifierData[]; + trainerItems: TrainerItemSaveData; + enemyTrainerItems: TrainerItemSaveData; arena: ArenaData; pokeballCounts: PokeballCounts; money: number; @@ -917,8 +914,8 @@ export class GameData { gameMode: globalScene.gameMode.modeId, party: globalScene.getPlayerParty().map(p => new PokemonData(p)), enemyParty: globalScene.getEnemyParty().map(p => new PokemonData(p)), - modifiers: globalScene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), - enemyModifiers: globalScene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), + trainerItems: globalScene.trainerItems.generateSaveData(), + enemyTrainerItems: globalScene.enemyTrainerItems.generateSaveData(), arena: new ArenaData(globalScene.arena), pokeballCounts: globalScene.pokeballCounts, money: Math.floor(globalScene.money), @@ -946,6 +943,7 @@ export class GameData { } const handleSessionData = async (sessionDataStr: string) => { try { + console.log(sessionDataStr); const sessionData = this.parseSessionData(sessionDataStr); resolve(sessionData); } catch (err) { @@ -1097,32 +1095,20 @@ export class GameData { } } + globalScene.trainerItems.clearItems(); + globalScene.assignTrainerItemsFromSaveData(sessionData.trainerItems, true); + globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag => loadPositionalTag(tag), ); - if (globalScene.modifiers.length) { - console.warn("Existing modifiers not cleared on session load, deleting..."); - globalScene.modifiers = []; - } - for (const modifierData of sessionData.modifiers) { - const modifier = modifierData.toModifier(Modifier[modifierData.className]); - if (modifier) { - globalScene.addModifier(modifier, true); - } - } - globalScene.updateModifiers(true); - - for (const enemyModifierData of sessionData.enemyModifiers) { - const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); - if (modifier) { - globalScene.addEnemyModifier(modifier, true); - } - } - - globalScene.updateModifiers(false); + globalScene.enemyTrainerItems.clearItems(); + globalScene.assignTrainerItemsFromSaveData(sessionData.enemyTrainerItems, false); Promise.all(loadPokemonAssets).then(() => resolve(true)); + + globalScene.updateItems(true); + globalScene.updateItems(false); }; if (sessionData) { initSessionFromData(sessionData); @@ -1267,25 +1253,12 @@ export class GameData { case "trainer": return v ? new TrainerData(v) : null; - case "modifiers": - case "enemyModifiers": { - const ret: PersistentModifierData[] = []; + // TODO: Figure out what to do with this + case "trainerItems": + case "enemyTrainerItems": { + const ret: TrainerItemConfiguration = []; for (const md of v ?? []) { - if (md?.className === "ExpBalanceModifier") { - // Temporarily limit EXP Balance until it gets reworked - md.stackCount = Math.min(md.stackCount, 4); - } - - if ( - md instanceof Modifier.EnemyAttackStatusEffectChanceModifier && - (md.effect === StatusEffect.FREEZE || md.effect === StatusEffect.SLEEP) - ) { - // Discard any old "sleep/freeze chance tokens". - // TODO: make this migrate script - continue; - } - - ret.push(new PersistentModifierData(md, k === "modifiers")); + ret.push(md); } return ret; } @@ -1326,6 +1299,17 @@ export class GameData { if (sync) { globalScene.ui.savingIcon.show(); } + if (useCachedSession) { + console.log("REPARSING!"); + console.log( + decrypt( + localStorage.getItem( + `sessionData${globalScene.sessionSlotId ? globalScene.sessionSlotId : ""}_${loggedInUser?.username}`, + )!, + bypassLogin, + ), + ); + } const sessionData = useCachedSession ? this.parseSessionData( decrypt( @@ -1361,6 +1345,7 @@ export class GameData { `sessionData${globalScene.sessionSlotId ? globalScene.sessionSlotId : ""}_${loggedInUser?.username}`, encrypt(JSON.stringify(sessionData), bypassLogin), ); + console.log(JSON.stringify(sessionData)); console.debug("Session data saved!"); diff --git a/src/system/modifier-data.ts b/src/system/modifier-data.ts deleted file mode 100644 index 135d1907e08..00000000000 --- a/src/system/modifier-data.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import { PersistentModifier } from "#modifiers/modifier"; -import type { GeneratedPersistentModifierType, ModifierType } from "#modifiers/modifier-type"; -import { getModifierTypeFuncById, ModifierTypeGenerator } from "#modifiers/modifier-type"; - -export class ModifierData { - public player: boolean; - public typeId: string; - public typePregenArgs: any[]; - public args: any[]; - public stackCount: number; - - public className: string; - - constructor(source: PersistentModifier | any, player: boolean) { - const sourceModifier = source instanceof PersistentModifier ? (source as PersistentModifier) : null; - this.player = player; - this.typeId = sourceModifier ? sourceModifier.type.id : source.typeId; - if (sourceModifier) { - if ("getPregenArgs" in source.type) { - this.typePregenArgs = (source.type as GeneratedPersistentModifierType).getPregenArgs(); - } - } else if (source.typePregenArgs) { - this.typePregenArgs = source.typePregenArgs; - } - this.args = sourceModifier ? sourceModifier.getArgs() : source.args || []; - this.stackCount = source.stackCount; - this.className = sourceModifier ? sourceModifier.constructor.name : source.className; - } - - toModifier(_constructor: any): PersistentModifier | null { - const typeFunc = getModifierTypeFuncById(this.typeId); - if (!typeFunc) { - return null; - } - - try { - let type: ModifierType | null = typeFunc(); - type.id = this.typeId; - - if (type instanceof ModifierTypeGenerator) { - type = (type as ModifierTypeGenerator).generateType( - this.player ? globalScene.getPlayerParty() : globalScene.getEnemyField(), - this.typePregenArgs, - ); - } - - const ret = Reflect.construct( - _constructor, - ([type] as any[]).concat(this.args).concat(this.stackCount), - ) as PersistentModifier; - - if (ret.stackCount > ret.getMaxStackCount()) { - ret.stackCount = ret.getMaxStackCount(); - } - - return ret; - } catch (err) { - console.error(err); - return null; - } - } -} diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 0ddfedeff84..055c0eb8172 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -11,6 +11,8 @@ import type { PokemonType } from "#enums/pokemon-type"; import type { SpeciesId } from "#enums/species-id"; import { TrainerSlot } from "#enums/trainer-slot"; import { EnemyPokemon, Pokemon } from "#field/pokemon"; +import type { HeldItemSaveData } from "#items/held-item-data-types"; +import { saveDataToConfig } from "#items/held-item-pool"; import { PokemonMove } from "#moves/pokemon-move"; import type { Variant } from "#sprites/variant"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -34,6 +36,7 @@ export class PokemonData { public stats: number[]; public ivs: number[]; public nature: Nature; + public heldItems: HeldItemSaveData; public moveset: PokemonMove[]; public status: Status | null; public friendship: number; @@ -101,6 +104,9 @@ export class PokemonData { this.hp = source.hp; this.stats = source.stats; this.ivs = source.ivs; + console.log("SAVE ITEMS:", sourcePokemon?.heldItemManager.generateSaveData()); + console.log(sourcePokemon, sourcePokemon?.heldItemManager); + this.heldItems = sourcePokemon?.heldItemManager.generateSaveData() ?? source.heldItems; // TODO: Can't we move some of this verification stuff to an upgrade script? this.nature = source.nature ?? Nature.HARDY; @@ -153,6 +159,8 @@ export class PokemonData { toPokemon(battleType?: BattleType, partyMemberIndex = 0, double = false): Pokemon { const species = getPokemonSpecies(this.species); + console.log("LOADED ITEMS:", this.heldItems); + console.log(saveDataToConfig(this.heldItems)); const ret: Pokemon = this.player ? globalScene.addPlayerPokemon( species, @@ -164,6 +172,7 @@ export class PokemonData { this.variant, this.ivs, this.nature, + saveDataToConfig(this.heldItems), this, playerPokemon => { if (this.nickname) { @@ -181,6 +190,7 @@ export class PokemonData { : TrainerSlot.NONE, this.boss, false, + saveDataToConfig(this.heldItems), this, ); diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index 32d9e0ee2be..c5b13ad576d 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -66,7 +66,7 @@ const TOUCH_CONTROLS_OPTIONS: SettingOption[] = [ const SHOP_CURSOR_TARGET_OPTIONS: SettingOption[] = [ { value: "Rewards", - label: i18next.t("settings:rewards"), + label: i18next.t("settings:allRewards"), }, { value: "Shop", diff --git a/src/system/version-migration/versions/v1_0_4.ts b/src/system/version-migration/versions/v1_0_4.ts index 2c50e05d40f..643737d7645 100644 --- a/src/system/version-migration/versions/v1_0_4.ts +++ b/src/system/version-migration/versions/v1_0_4.ts @@ -137,7 +137,7 @@ const migrateModifiers: SessionSaveMigrator = { m.className = "ResetNegativeStatStageModifier"; } else if (m.className === "TempBattleStatBoosterModifier") { const maxBattles = 5; - // Dire Hit no longer a part of the TempBattleStatBoosterModifierTypeGenerator + // Dire Hit no longer a part of the TempBattleStatBoosterRewardGenerator if (m.typeId !== "DIRE_HIT") { m.className = "TempStatStageBoosterModifier"; m.typeId = "TEMP_STAT_STAGE_BOOSTER"; diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 9877f298404..34223f5d6a1 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -5,6 +5,7 @@ import { Challenges } from "#enums/challenges"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { TextStyle } from "#enums/text-style"; import { WeatherType } from "#enums/weather-type"; import { addTextObject } from "#ui/text"; @@ -60,12 +61,12 @@ interface TimedEvent extends EventBanner { startDate: Date; endDate: Date; eventEncounters?: EventEncounter[]; - delibirdyBuff?: string[]; + delibirdyBuff?: TrainerItemId[]; weather?: WeatherPoolEntry[]; mysteryEncounterTierChanges?: EventMysteryEncounterTier[]; luckBoostedSpecies?: SpeciesId[]; boostFusions?: boolean; //MODIFIER REWORK PLEASE - classicWaveRewards?: EventWaveReward[]; // Rival battle rewards + classicWaveRewards?: EventWaveReward[]; // Rival battle allRewards trainerShinyChance?: number; // Odds over 65536 of trainer mon generating as shiny music?: EventMusicReplacement[]; dailyRunChallenges?: EventChallenge[]; @@ -104,7 +105,14 @@ const timedEvents: TimedEvent[] = [ { species: SpeciesId.GALAR_DARUMAKA }, { species: SpeciesId.IRON_BUNDLE }, ], - delibirdyBuff: ["CATCHING_CHARM", "SHINY_CHARM", "ABILITY_CHARM", "EXP_CHARM", "SUPER_EXP_CHARM", "HEALING_CHARM"], + delibirdyBuff: [ + TrainerItemId.CATCHING_CHARM, + TrainerItemId.SHINY_CHARM, + TrainerItemId.ABILITY_CHARM, + TrainerItemId.EXP_CHARM, + TrainerItemId.SUPER_EXP_CHARM, + TrainerItemId.HEALING_CHARM, + ], weather: [{ weatherType: WeatherType.SNOW, weight: 1 }], mysteryEncounterTierChanges: [ { @@ -459,10 +467,10 @@ export class TimedEventManager { /** * For events where Delibirdy gives extra items - * @returns list of ids of {@linkcode ModifierType}s that Delibirdy hands out as a bonus + * @returns list of ids of {@linkcode Reward}s that Delibirdy hands out as a bonus */ - getDelibirdyBuff(): string[] { - const ret: string[] = []; + getDelibirdyBuff(): TrainerItemId[] { + const ret: TrainerItemId[] = []; timedEvents .filter(te => this.isActive(te)) .map(te => { @@ -560,7 +568,7 @@ export class TimedEventManager { /** * Gets all the modifier types associated with a certain wave during an event * @see EventWaveReward - * @param wave the wave to check for associated rewards + * @param wave the wave to check for associated allRewards * @returns array of strings of the event modifier reward types */ getFixedBattleEventRewards(wave: number): string[] { diff --git a/src/tutorial.ts b/src/tutorial.ts index 018d0927da0..93e11335c65 100644 --- a/src/tutorial.ts +++ b/src/tutorial.ts @@ -98,7 +98,7 @@ const tutorialHandlers = { null, () => globalScene.ui.showText("", null, () => - globalScene.ui.setModeWithoutClear(UiMode.MODIFIER_SELECT).then(() => resolve()), + globalScene.ui.setModeWithoutClear(UiMode.REWARD_SELECT).then(() => resolve()), ), null, true, diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 71c5ac1049e..cf340cf3ea7 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -185,7 +185,7 @@ export class UiInputs { } case UiMode.TITLE: case UiMode.COMMAND: - case UiMode.MODIFIER_SELECT: + case UiMode.REWARD_SELECT: case UiMode.MYSTERY_ENCOUNTER: globalScene.ui.setOverlayMode(UiMode.MENU); break; diff --git a/src/ui/battle-flyout.ts b/src/ui/battle-flyout.ts index 0a67dc9ad37..c0193825d10 100644 --- a/src/ui/battle-flyout.ts +++ b/src/ui/battle-flyout.ts @@ -178,8 +178,8 @@ export class BattleFlyout extends Phaser.GameObjects.Container { const berryUsedEvent = event as BerryUsedEvent; if ( !berryUsedEvent || - berryUsedEvent.berryModifier.pokemonId !== this.pokemon?.id || - berryUsedEvent.berryModifier.berryType !== BerryType.LEPPA + berryUsedEvent.pokemon.id !== this.pokemon?.id || + berryUsedEvent.berryType !== BerryType.LEPPA ) { // We only care about Leppa berries return; diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 41ff559062a..9adf6b7f9de 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -5,9 +5,9 @@ import { Button } from "#enums/buttons"; import { Command } from "#enums/command"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { TerastallizeAccessModifier } from "#modifiers/modifier"; import type { CommandPhase } from "#phases/command-phase"; import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import { addTextObject } from "#ui/text"; @@ -198,7 +198,7 @@ export class CommandUiHandler extends UiHandler { } canTera(): boolean { - const hasTeraMod = !!globalScene.getModifiers(TerastallizeAccessModifier).length; + const hasTeraMod = !!globalScene.trainerItems.hasItem(TrainerItemId.TERA_ORB); const activePokemon = globalScene.getField()[this.fieldIndex]; const isBlockedForm = activePokemon.isMega() || activePokemon.isMax() || activePokemon.hasSpecies(SpeciesId.NECROZMA, "ultra"); diff --git a/src/ui/egg-hatch-scene-handler.ts b/src/ui/egg-hatch-scene-handler.ts index 5b2c9d40cfa..dda259e899a 100644 --- a/src/ui/egg-hatch-scene-handler.ts +++ b/src/ui/egg-hatch-scene-handler.ts @@ -37,7 +37,7 @@ export class EggHatchSceneHandler extends UiHandler { this.getUi().showText("", 0); - globalScene.setModifiersVisible(false); + globalScene.setItemsVisible(false); return true; } diff --git a/src/ui/item-bar-ui.ts b/src/ui/item-bar-ui.ts new file mode 100644 index 00000000000..85c81bb204b --- /dev/null +++ b/src/ui/item-bar-ui.ts @@ -0,0 +1,107 @@ +import { globalScene } from "#app/global-scene"; +import { allHeldItems, allTrainerItems } from "#data/data-lists"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { TrainerItemId } from "#enums/trainer-item-id"; +import type { Pokemon } from "#field/pokemon"; +import { heldItemSortFunc, trainerItemSortFunc } from "#items/item-utility"; +import type { TrainerItemManager } from "#items/trainer-item-manager"; + +const iconOverflowIndex = 24; + +export class ItemBar extends Phaser.GameObjects.Container { + private player: boolean; + private itemCache: (HeldItemId | TrainerItemId)[]; + public totalVisibleLength = 0; + + constructor(enemy?: boolean) { + super(globalScene, 1 + (enemy ? 302 : 0), 2); + + this.player = !enemy; + this.setScale(0.5); + } + + /** + * Method to update content displayed in {@linkcode ItemBar} + * @param {PersistentItem[]} items - The list of items to be displayed in the {@linkcode ItemBar} + * @param {boolean} hideHeldItems - If set to "true", only items not assigned to a Pokémon are displayed + */ + updateItems(trainerItems: TrainerItemManager, pokemonA?: Pokemon, pokemonB?: Pokemon) { + this.removeAll(true); + + const sortedTrainerItems = trainerItems.getTrainerItems().sort(trainerItemSortFunc); + + const heldItemsA = pokemonA ? pokemonA.getHeldItems().sort(heldItemSortFunc) : []; + const heldItemsB = pokemonB ? pokemonB.getHeldItems().sort(heldItemSortFunc) : []; + + this.totalVisibleLength = sortedTrainerItems.length + heldItemsA.length + heldItemsB.length; + + let iconCount = 0; + sortedTrainerItems.forEach(item => { + const icon = allTrainerItems[item].createIcon(trainerItems.getStack(item)); + iconCount += 1; + this.addIcon(icon, iconCount, allTrainerItems[item].name, allTrainerItems[item].description); + }); + + if (pokemonA) { + heldItemsA.forEach(item => { + const icon = allHeldItems[item].createPokemonIcon(pokemonA); + iconCount += 1; + this.addIcon(icon, iconCount, allHeldItems[item].name, allHeldItems[item].description); + }); + } + + if (pokemonB) { + heldItemsB.forEach(item => { + const icon = allHeldItems[item].createPokemonIcon(pokemonB); + iconCount += 1; + this.addIcon(icon, iconCount, allHeldItems[item].name, allHeldItems[item].description); + }); + } + + for (const icon of this.getAll()) { + this.sendToBack(icon); + } + + this.itemCache = ([] as (TrainerItemId | HeldItemId)[]) + .concat(sortedTrainerItems) + .concat(heldItemsA) + .concat(heldItemsB); + } + + addIcon(icon: Phaser.GameObjects.Container, i: number, name: string, description: string) { + if (i >= iconOverflowIndex) { + icon.setVisible(false); + } + this.add(icon); + this.setItemIconPosition(icon, this.totalVisibleLength); + icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 24), Phaser.Geom.Rectangle.Contains); + icon.on("pointerover", () => { + globalScene.ui.showTooltip(name, description); + if (this.itemCache && this.itemCache.length > iconOverflowIndex) { + this.updateItemOverflowVisibility(true); + } + }); + icon.on("pointerout", () => { + globalScene.ui.hideTooltip(); + if (this.itemCache && this.itemCache.length > iconOverflowIndex) { + this.updateItemOverflowVisibility(false); + } + }); + } + + updateItemOverflowVisibility(ignoreLimit: boolean) { + const itemIcons = this.getAll().reverse(); + for (const item of itemIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) { + item.setVisible(ignoreLimit); + } + } + + setItemIconPosition(icon: Phaser.GameObjects.Container, itemCount: number) { + const rowIcons: number = 12 + 6 * Math.max(Math.ceil(Math.min(itemCount, 24) / 12) - 2, 0); + + const x = ((this.getIndex(icon) % rowIcons) * 26) / (rowIcons / 12); + const y = Math.floor(this.getIndex(icon) / rowIcons) * 20; + + icon.setPosition(this.player ? x : -x, y); + } +} diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index fa65cccab2f..a18b60ac6f9 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -125,7 +125,7 @@ export class MenuUiHandler extends MessageUiHandler { const ui = this.getUi(); this.excludedMenus = () => [ { - condition: !!globalScene.phaseManager.getCurrentPhase()?.is("SelectModifierPhase"), + condition: !!globalScene.phaseManager.getCurrentPhase()?.is("SelectRewardPhase"), options: [MenuOptions.EGG_GACHA], }, { condition: bypassLogin, options: [MenuOptions.LOG_OUT] }, diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 0337e487200..183126303eb 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -2,13 +2,14 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { pokemonEvolutions } from "#balance/pokemon-evolutions"; import { applyChallenges } from "#data/challenge"; -import { allMoves } from "#data/data-lists"; +import { allHeldItems, allMoves } from "#data/data-lists"; import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; import { Gender, getGenderColor, getGenderSymbol } from "#data/gender"; import { Button } from "#enums/buttons"; import { ChallengeType } from "#enums/challenge-type"; import { Command } from "#enums/command"; import { FormChangeItem } from "#enums/form-change-item"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; @@ -16,7 +17,7 @@ import { StatusEffect } from "#enums/status-effect"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; -import type { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; +import { formChangeItemName } from "#items/item-utility"; import type { PokemonMove } from "#moves/pokemon-move"; import type { CommandPhase } from "#phases/command-phase"; import { getVariantTint } from "#sprites/variant"; @@ -62,27 +63,27 @@ export enum PartyUiMode { * Indicates that the party UI is open to select a mon to apply a modifier to. * This type of selection can be cancelled. */ - MODIFIER, + REWARD, /** * Indicates that the party UI is open to select a mon to apply a move * modifier to (such as an Ether or PP Up). This type of selection can be cancelled. */ - MOVE_MODIFIER, + MOVE_REWARD, /** * Indicates that the party UI is open to select a mon to teach a TM. This * type of selection can be cancelled. */ - TM_MODIFIER, + TM_REWARD, /** * Indicates that the party UI is open to select a mon to remember a move. * This type of selection can be cancelled. */ - REMEMBER_MOVE_MODIFIER, + REMEMBER_MOVE_REWARD, /** * Indicates that the party UI is open to transfer items between mons. This * type of selection can be cancelled. */ - MODIFIER_TRANSFER, + ITEM_TRANSFER, /** * Indicates that the party UI is open because of a DNA Splicer. This * type of selection can be cancelled. @@ -138,18 +139,15 @@ export enum PartyOption { } export type PartySelectCallback = (cursor: number, option: PartyOption) => void; -export type PartyModifierTransferSelectCallback = ( +export type PartyItemTransferSelectCallback = ( fromCursor: number, index: number, itemQuantity?: number, toCursor?: number, ) => void; -export type PartyModifierSpliceSelectCallback = (fromCursor: number, toCursor?: number) => void; +export type PartyRewardSpliceSelectCallback = (fromCursor: number, toCursor?: number) => void; export type PokemonSelectFilter = (pokemon: PlayerPokemon) => string | null; -export type PokemonModifierTransferSelectFilter = ( - pokemon: PlayerPokemon, - modifier: PokemonHeldItemModifier, -) => string | null; +export type PokemonItemTransferSelectFilter = (pokemon: PlayerPokemon, item: HeldItemId) => string | null; export type PokemonMoveSelectFilter = (pokemonMove: PokemonMove) => string | null; export class PartyUiHandler extends MessageUiHandler { @@ -189,8 +187,8 @@ export class PartyUiHandler extends MessageUiHandler { private lastCursor = 0; private lastLeftPokemonCursor = 0; private lastRightPokemonCursor = 0; - private selectCallback: PartySelectCallback | PartyModifierTransferSelectCallback | null; - private selectFilter: PokemonSelectFilter | PokemonModifierTransferSelectFilter; + private selectCallback: PartySelectCallback | PokemonItemTransferSelectFilter | null; + private selectFilter: PokemonSelectFilter | PokemonItemTransferSelectFilter; private moveSelectFilter: PokemonMoveSelectFilter; private tmMoveId: MoveId; private showMovePp: boolean; @@ -231,11 +229,8 @@ export 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; @@ -530,19 +525,20 @@ export 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.selectCallback as PartyItemTransferSelectCallback)( this.transferCursor, invertedIndex, this.transferQuantitiesMax[invertedIndex], this.cursor, ); - }, - ); + }); } else { - (this.selectCallback as PartyModifierTransferSelectCallback)( + (this.selectCallback as PartyItemTransferSelectCallback)( this.transferCursor, this.transferOptionCursor, this.transferQuantities[this.transferOptionCursor], @@ -557,7 +553,7 @@ export class PartyUiHandler extends MessageUiHandler { } // TODO: This will be largely changed with the modifier rework - private processModifierTransferModeInput(pokemon: PlayerPokemon) { + private processItemTransferModeInput(pokemon: PlayerPokemon) { const ui = this.getUi(); const option = this.options[this.optionsCursor]; @@ -575,18 +571,15 @@ export 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 { @@ -615,7 +608,7 @@ export class PartyUiHandler extends MessageUiHandler { } // TODO: Might need to check here for when this.transferMode is active. - private processModifierTransferModeLeftRightInput(button: Button) { + private processItemTransferModeLeftRightInput(button: Button) { let success = false; const option = this.options[this.optionsCursor]; if (button === Button.LEFT) { @@ -649,11 +642,11 @@ export class PartyUiHandler extends MessageUiHandler { } // TODO: Might need to check here for when this.transferMode is active. - private processModifierTransferModeUpDownInput(button: Button.UP | Button.DOWN) { + private processItemTransferModeUpDownInput(button: Button.UP | Button.DOWN) { let success = false; const option = this.options[this.optionsCursor]; - if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) { + if (this.partyUiMode === PartyUiMode.ITEM_TRANSFER) { if (option !== PartyOption.ALL) { this.transferQuantities[option] = this.transferQuantitiesMax[option]; } @@ -747,12 +740,6 @@ export 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) { @@ -760,13 +747,13 @@ export class PartyUiHandler extends MessageUiHandler { if (filterResult === null && (option === PartyOption.SEND_OUT || option === PartyOption.PASS_BATON)) { filterResult = this.FilterChallengeLegal(pokemon); } - if (filterResult === null && this.partyUiMode === PartyUiMode.MOVE_MODIFIER) { + if (filterResult === null && this.partyUiMode === PartyUiMode.MOVE_REWARD) { filterResult = this.moveSelectFilter(pokemon.moveset[this.optionsCursor]); } } else { - filterResult = (this.selectFilter as PokemonModifierTransferSelectFilter)( + filterResult = (this.selectFilter as PokemonItemTransferSelectFilter)( pokemon, - this.getTransferrableItemsFromPokemon(globalScene.getPlayerParty()[this.transferCursor])[ + globalScene.getPlayerParty()[this.transferCursor].heldItemManager.getTransferableHeldItems()[ this.transferOptionCursor ], ); @@ -785,8 +772,12 @@ export class PartyUiHandler extends MessageUiHandler { // TODO: Careful about using success for the return values here. Find a better way // PartyOption.ALL, and options specific to the mode (held items) - if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) { - return this.processModifierTransferModeInput(pokemon); + if (this.partyUiMode === PartyUiMode.ITEM_TRANSFER) { + return this.processItemTransferModeInput(pokemon); + } + + if (this.partyUiMode === PartyUiMode.DISCARD) { + return this.processDiscardMenuInput(pokemon); } if (this.partyUiMode === PartyUiMode.DISCARD) { @@ -794,7 +785,7 @@ export class PartyUiHandler extends MessageUiHandler { } // options specific to the mode (moves) - if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { + if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_REWARD) { return this.processRememberMoveModeInput(pokemon); } @@ -818,12 +809,12 @@ export class PartyUiHandler extends MessageUiHandler { // TODO: This risks hitting the other options (.MOVE_i and ALL) so does it? Do we need an extra check? if ( option >= PartyOption.FORM_CHANGE_ITEM && - globalScene.phaseManager.getCurrentPhase()?.is("SelectModifierPhase") && + globalScene.phaseManager.getCurrentPhase()?.is("SelectRewardPhase") && 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); } @@ -845,19 +836,19 @@ export class PartyUiHandler extends MessageUiHandler { // PartyUiMode.RELEASE (RELEASE) // PartyUiMode.FAINT_SWITCH (SEND_OUT or PASS_BATON (?)) // PartyUiMode.REVIVAL_BLESSING (REVIVE) - // PartyUiMode.MODIFIER_TRANSFER (held items, and ALL) + // PartyUiMode.ITEM_TRANSFER (held items, and ALL) // PartyUiMode.CHECK --- no specific option, only relevant on cancel? // PartyUiMode.SPLICE (SPLICE) - // PartyUiMode.MOVE_MODIFIER (MOVE_1, MOVE_2, MOVE_3, MOVE_4) - // PartyUiMode.TM_MODIFIER (TEACH) - // PartyUiMode.REMEMBER_MOVE_MODIFIER (no specific option, callback is invoked when selecting a move) - // PartyUiMode.MODIFIER (APPLY option) + // PartyUiMode.MOVE_REWARD (MOVE_1, MOVE_2, MOVE_3, MOVE_4) + // PartyUiMode.TM_REWARD (TEACH) + // PartyUiMode.REMEMBER_MOVE_REWARD (no specific option, callback is invoked when selecting a move) + // PartyUiMode.REWARD (APPLY option) // PartyUiMode.POST_BATTLE_SWITCH (SEND_OUT) // These are the options that need a callback if (this.partyUiMode === PartyUiMode.SPLICE) { if (option === PartyOption.SPLICE) { - (this.selectCallback as PartyModifierSpliceSelectCallback)(this.transferCursor, this.cursor); + (this.selectCallback as PartyRewardSpliceSelectCallback)(this.transferCursor, this.cursor); this.clearTransfer(); } else if (option === PartyOption.APPLY) { this.startTransfer(); @@ -921,11 +912,11 @@ export class PartyUiHandler extends MessageUiHandler { } if (button === Button.UP || button === Button.DOWN) { - if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) { - return this.processModifierTransferModeUpDownInput(button); + if (this.partyUiMode === PartyUiMode.ITEM_TRANSFER) { + return this.processItemTransferModeUpDownInput(button); } - if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { + if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_REWARD) { return this.processRememberMoveModeUpDownInput(button); } @@ -934,7 +925,7 @@ export class PartyUiHandler extends MessageUiHandler { if (button === Button.LEFT || button === Button.RIGHT) { if (this.isItemManageMode()) { - return this.processModifierTransferModeLeftRightInput(button); + return this.processItemTransferModeLeftRightInput(button); } } @@ -1001,18 +992,14 @@ export class PartyUiHandler extends MessageUiHandler { const ui = this.getUi(); if (this.cursor < 6) { if ( - (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) || + (this.partyUiMode === PartyUiMode.ITEM_TRANSFER && !this.transferMode) || this.partyUiMode === PartyUiMode.DISCARD ) { /** 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(); @@ -1050,7 +1037,7 @@ export class PartyUiHandler extends MessageUiHandler { private processPartyCancelInput(): boolean { const ui = this.getUi(); if ( - (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER || this.partyUiMode === PartyUiMode.SPLICE) && + (this.partyUiMode === PartyUiMode.ITEM_TRANSFER || this.partyUiMode === PartyUiMode.SPLICE) && this.transferMode ) { this.clearTransfer(); @@ -1279,10 +1266,10 @@ export class PartyUiHandler extends MessageUiHandler { let optionsMessage = i18next.t("partyUiHandler:doWhatWithThisPokemon"); switch (this.partyUiMode) { - case PartyUiMode.MOVE_MODIFIER: + case PartyUiMode.MOVE_REWARD: optionsMessage = i18next.t("partyUiHandler:selectAMove"); break; - case PartyUiMode.MODIFIER_TRANSFER: + case PartyUiMode.ITEM_TRANSFER: if (!this.transferMode) { optionsMessage = i18next.t("partyUiHandler:changeQuantity"); } @@ -1312,7 +1299,7 @@ export class PartyUiHandler extends MessageUiHandler { showPartyText() { switch (this.partyUiMode) { - case PartyUiMode.MODIFIER_TRANSFER: + case PartyUiMode.ITEM_TRANSFER: this.showText(i18next.t("partyUiHandler:partyTransfer")); break; case PartyUiMode.DISCARD: @@ -1324,12 +1311,10 @@ export class PartyUiHandler extends MessageUiHandler { } } - private allowBatonModifierSwitch(): boolean { + private allowBatonSwitch(): 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) ); } @@ -1344,15 +1329,7 @@ export 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 { + private updateOptionsWithRememberMoveRewardMode(pokemon): void { const learnableMoves = pokemon.getLearnableLevelMoves(); for (let m = 0; m < learnableMoves.length; m++) { this.options.push(m); @@ -1363,19 +1340,19 @@ export class PartyUiHandler extends MessageUiHandler { } } - private updateOptionsWithMoveModifierMode(pokemon): void { + private updateOptionsWithMoveRewardMode(pokemon): void { // MOVE_1, MOVE_2, MOVE_3, MOVE_4 for (let m = 0; m < pokemon.moveset.length; m++) { this.options.push(PartyOption.MOVE_1 + m); } } - private updateOptionsWithModifierTransferMode(pokemon): void { - const itemModifiers = this.getItemModifiers(pokemon); - for (let im = 0; im < itemModifiers.length; im++) { + private updateOptionsWithItemTransferMode(pokemon): void { + 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); } } @@ -1427,15 +1404,15 @@ export class PartyUiHandler extends MessageUiHandler { } switch (this.partyUiMode) { - case PartyUiMode.MOVE_MODIFIER: - this.updateOptionsWithMoveModifierMode(pokemon); + case PartyUiMode.MOVE_REWARD: + this.updateOptionsWithMoveRewardMode(pokemon); break; - case PartyUiMode.REMEMBER_MOVE_MODIFIER: - this.updateOptionsWithRememberMoveModifierMode(pokemon); + case PartyUiMode.REMEMBER_MOVE_REWARD: + this.updateOptionsWithRememberMoveRewardMode(pokemon); break; - case PartyUiMode.MODIFIER_TRANSFER: + case PartyUiMode.ITEM_TRANSFER: if (!this.transferMode) { - this.updateOptionsWithModifierTransferMode(pokemon); + this.updateOptionsWithItemTransferMode(pokemon); } else { this.options.push(PartyOption.TRANSFER); this.addCommonOptions(pokemon); @@ -1451,21 +1428,19 @@ export class PartyUiHandler extends MessageUiHandler { case PartyUiMode.FAINT_SWITCH: case PartyUiMode.POST_BATTLE_SWITCH: if (this.cursor >= globalScene.currentBattle.getBattlerCount()) { - const allowBatonModifierSwitch = this.allowBatonModifierSwitch(); + const allowBatonSwitch = this.allowBatonSwitch(); const isBatonPassMove = this.isBatonPassMove(); - if (allowBatonModifierSwitch && !isBatonPassMove) { + if (allowBatonSwitch && !isBatonPassMove) { // the BATON modifier gives an extra switch option for // pokemon-command switches, allowing buffs to be optionally passed this.options.push(PartyOption.PASS_BATON); } - // isBatonPassMove and allowBatonModifierSwitch shouldn't ever be true + // isBatonPassMove and allowBatonItemSwitch shouldn't ever be true // at the same time, because they both explicitly check for a mutually // exclusive partyUiMode. But better safe than sorry. - this.options.push( - isBatonPassMove && !allowBatonModifierSwitch ? PartyOption.PASS_BATON : PartyOption.SEND_OUT, - ); + this.options.push(isBatonPassMove && !allowBatonSwitch ? PartyOption.PASS_BATON : PartyOption.SEND_OUT); } this.addCommonOptions(pokemon); if (this.partyUiMode === PartyUiMode.SWITCH) { @@ -1479,11 +1454,11 @@ export class PartyUiHandler extends MessageUiHandler { this.options.push(PartyOption.REVIVE); this.addCommonOptions(pokemon); break; - case PartyUiMode.MODIFIER: + case PartyUiMode.REWARD: this.options.push(PartyOption.APPLY); this.addCommonOptions(pokemon); break; - case PartyUiMode.TM_MODIFIER: + case PartyUiMode.TM_REWARD: this.options.push(PartyOption.TEACH); this.addCommonOptions(pokemon); break; @@ -1506,9 +1481,9 @@ export class PartyUiHandler extends MessageUiHandler { break; case PartyUiMode.CHECK: this.addCommonOptions(pokemon); - if (globalScene.phaseManager.getCurrentPhase()?.is("SelectModifierPhase")) { - const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon); - for (let i = 0; i < formChangeItemModifiers.length; i++) { + if (globalScene.phaseManager.getCurrentPhase()?.is("SelectRewardPhase")) { + const formChangeItems = this.getFormChangeItems(pokemon); + for (let i = 0; i < formChangeItems.length; i++) { this.options.push(PartyOption.FORM_CHANGE_ITEM + i); } } @@ -1548,8 +1523,8 @@ export class PartyUiHandler extends MessageUiHandler { } else if (option === PartyOption.SCROLL_DOWN) { optionName = "↓"; } else if ( - (this.partyUiMode !== PartyUiMode.REMEMBER_MOVE_MODIFIER && - (this.partyUiMode !== PartyUiMode.MODIFIER_TRANSFER || this.transferMode) && + (this.partyUiMode !== PartyUiMode.REMEMBER_MOVE_REWARD && + (this.partyUiMode !== PartyUiMode.ITEM_TRANSFER || this.transferMode) && this.partyUiMode !== PartyUiMode.DISCARD) || option === PartyOption.CANCEL ) { @@ -1569,10 +1544,10 @@ export 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 { @@ -1585,7 +1560,7 @@ export class PartyUiHandler extends MessageUiHandler { break; } } - } else if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { + } else if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_REWARD) { const learnableLevelMoves = pokemon.getLearnableLevelMoves(); const move = learnableLevelMoves[option]; optionName = allMoves[move].name; @@ -1596,9 +1571,9 @@ export 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; @@ -1610,19 +1585,19 @@ export 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.isItemManageMode() && 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]`; } @@ -1670,7 +1645,6 @@ export class PartyUiHandler extends MessageUiHandler { null, () => { this.clearPartySlots(); - globalScene.removePartyMemberModifiers(slotIndex); const releasedPokemon = globalScene.getPlayerParty().splice(slotIndex, 1)[0]; releasedPokemon.destroy(); this.populatePartySlots(); @@ -1731,29 +1705,24 @@ export 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 { @@ -2020,7 +1989,7 @@ class PartySlot extends Phaser.GameObjects.Container { slotInfoContainer.add([this.slotHpBar, this.slotHpOverlay, this.slotHpText, this.slotDescriptionLabel]); - if (partyUiMode !== PartyUiMode.TM_MODIFIER) { + if (partyUiMode !== PartyUiMode.TM_REWARD) { this.slotDescriptionLabel.setVisible(false); this.slotHpBar.setVisible(true); this.slotHpOverlay.setVisible(true); diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 3b2349348a8..15498c68c9d 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -435,7 +435,7 @@ export class PokemonInfoContainer extends Phaser.GameObjects.Container { this.setVisible(true); this.shown = true; - globalScene.hideEnemyModifierBar(); + globalScene.hideEnemyItemBar(); }); } @@ -489,7 +489,7 @@ export class PokemonInfoContainer extends Phaser.GameObjects.Container { hide(speedMultiplier = 1): Promise { return new Promise(resolve => { if (!this.shown) { - globalScene.showEnemyModifierBar(); + globalScene.showEnemyItemBar(); return resolve(); } @@ -510,7 +510,7 @@ export class PokemonInfoContainer extends Phaser.GameObjects.Container { this.pokemonShinyIcon.off("pointerover"); this.pokemonShinyIcon.off("pointerout"); globalScene.ui.hideTooltip(); - globalScene.showEnemyModifierBar(); + globalScene.showEnemyItemBar(); resolve(); }, }); diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/reward-select-ui-handler.ts similarity index 89% rename from src/ui/modifier-select-ui-handler.ts rename to src/ui/reward-select-ui-handler.ts index 16eecf6993d..7b222460b97 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/reward-select-ui-handler.ts @@ -7,13 +7,14 @@ import { Button } from "#enums/buttons"; import type { PokeballType } from "#enums/pokeball"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; import { TextStyle } from "#enums/text-style"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; -import { HealShopCostModifier, LockModifierTiersModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; -import type { ModifierTypeOption } from "#modifiers/modifier-type"; -import { getPlayerShopModifierTypeOptionsForWave, TmModifierType } from "#modifiers/modifier-type"; +import type { RewardOption } from "#items/reward"; +import { getPlayerShopRewardOptionsForWave, isTmReward } from "#items/reward-utils"; +import { TrainerItemEffect } from "#items/trainer-item"; import { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; import { MoveInfoOverlay } from "#ui/move-info-overlay"; -import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions } from "#ui/text"; +import { addTextObject, getRarityTierTextTint, getTextColor, getTextStyleOptions } from "#ui/text"; import { formatMoney, NumberHolder } from "#utils/common"; import i18next from "i18next"; import Phaser from "phaser"; @@ -23,7 +24,7 @@ const SINGLE_SHOP_ROW_YOFFSET = 12; const DOUBLE_SHOP_ROW_YOFFSET = 24; const OPTION_BUTTON_YPOSITION = -62; -export class ModifierSelectUiHandler extends AwaitableUiHandler { +export class RewardSelectUiHandler extends AwaitableUiHandler { private modifierContainer: Phaser.GameObjects.Container; private rerollButtonContainer: Phaser.GameObjects.Container; private lockRarityButtonContainer: Phaser.GameObjects.Container; @@ -69,8 +70,9 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { if (context) { context.font = styleOptions.fontSize + "px " + styleOptions.fontFamily; - this.transferButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:manageItems")).width; - this.checkButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:checkTeam")).width; + // TODO @Wlowscha: Rename these locales + this.transferButtonWidth = context.measureText(i18next.t("rewardSelectUiHandler:manageItems")).width; + this.checkButtonWidth = context.measureText(i18next.t("rewardSelectUiHandler:checkTeam")).width; } this.transferButtonContainer = globalScene.add.container( @@ -81,7 +83,8 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.transferButtonContainer.setVisible(false); ui.add(this.transferButtonContainer); - const transferButtonText = addTextObject(-4, -2, i18next.t("modifierSelectUiHandler:manageItems"), TextStyle.PARTY); + // TODO @Wlowscha: Remember to rename these locales + const transferButtonText = addTextObject(-4, -2, i18next.t("rewardSelectUiHandler:manageItems"), TextStyle.PARTY); transferButtonText.setName("text-transfer-btn"); transferButtonText.setOrigin(1, 0); this.transferButtonContainer.add(transferButtonText); @@ -94,7 +97,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.checkButtonContainer.setVisible(false); ui.add(this.checkButtonContainer); - const checkButtonText = addTextObject(-4, -2, i18next.t("modifierSelectUiHandler:checkTeam"), TextStyle.PARTY); + const checkButtonText = addTextObject(-4, -2, i18next.t("rewardSelectUiHandler:checkTeam"), TextStyle.PARTY); checkButtonText.setName("text-use-btn"); checkButtonText.setOrigin(1, 0); this.checkButtonContainer.add(checkButtonText); @@ -104,7 +107,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.rerollButtonContainer.setVisible(false); ui.add(this.rerollButtonContainer); - const rerollButtonText = addTextObject(-4, -2, i18next.t("modifierSelectUiHandler:reroll"), TextStyle.PARTY); + const rerollButtonText = addTextObject(-4, -2, i18next.t("rewardSelectUiHandler:reroll"), TextStyle.PARTY); rerollButtonText.setName("text-reroll-btn"); rerollButtonText.setOrigin(0, 0); this.rerollButtonContainer.add(rerollButtonText); @@ -119,12 +122,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonContainer.setVisible(false); ui.add(this.lockRarityButtonContainer); - this.lockRarityButtonText = addTextObject( - -4, - -2, - i18next.t("modifierSelectUiHandler:lockRarities"), - TextStyle.PARTY, - ); + this.lockRarityButtonText = addTextObject(-4, -2, i18next.t("rewardSelectUiHandler:lockRarities"), TextStyle.PARTY); this.lockRarityButtonText.setOrigin(0, 0); this.lockRarityButtonContainer.add(this.lockRarityButtonText); @@ -139,7 +137,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { const continueButtonText = addTextObject( -24, 5, - i18next.t("modifierSelectUiHandler:continueNextWaveButton"), + i18next.t("rewardSelectUiHandler:continueNextWaveButton"), TextStyle.MESSAGE, ); continueButtonText.setName("text-continue-btn"); @@ -184,8 +182,11 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.player = args[0]; const partyHasHeldItem = - this.player && !!globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable).length; - const canLockRarities = !!globalScene.findModifier(m => m instanceof LockModifierTiersModifier); + globalScene + .getPlayerParty() + .map(p => p.heldItemManager.getTransferableHeldItems().length) + .reduce((tot, i) => tot + i, 0) > 0; + const canLockRarities = !!globalScene.trainerItems.hasItem(TrainerItemId.LOCK_CAPSULE); this.transferButtonContainer.setVisible(false); this.transferButtonContainer.setAlpha(0); @@ -208,12 +209,12 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.updateRerollCostText(); - const typeOptions = args[1] as ModifierTypeOption[]; + const typeOptions = args[1] as RewardOption[]; const removeHealShop = globalScene.gameMode.hasNoShop; const baseShopCost = new NumberHolder(globalScene.getWaveMoneyAmount(1)); - globalScene.applyModifier(HealShopCostModifier, true, baseShopCost); + globalScene.applyPlayerItems(TrainerItemEffect.HEAL_SHOP_COST, { numberHolder: baseShopCost }); const shopTypeOptions = !removeHealShop - ? getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value) + ? getPlayerShopRewardOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value) : []; const optionsYOffset = shopTypeOptions.length > SHOP_OPTIONS_ROW_LIMIT ? -SINGLE_SHOP_ROW_YOFFSET : -DOUBLE_SHOP_ROW_YOFFSET; @@ -259,10 +260,12 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.shopOptionsRows[row].push(option); } - const maxUpgradeCount = typeOptions.map(to => to.upgradeCount).reduce((max, current) => Math.max(current, max), 0); + const maxUpgradeCount = typeOptions + .map(to => to.upgradeCount ?? 0) + .reduce((max, current) => Math.max(current, max), 0); - /* Force updateModifiers without pokemonSpecificModifiers */ - globalScene.getModifierBar().updateModifiers(globalScene.modifiers, true); + /* Force updateItems without pokemon held items */ + globalScene.updateItems(true, false); /* Multiplies the appearance duration by the speed parameter so that it is always constant, and avoids "flashbangs" at game speed x5 */ globalScene.showShopOverlay(750 * globalScene.gameSpeed); @@ -562,7 +565,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { -globalScene.game.canvas.height / 12 - (this.shopOptionsRows.length > 1 ? SINGLE_SHOP_ROW_YOFFSET - 2 : DOUBLE_SHOP_ROW_YOFFSET - 2), ); - ui.showText(i18next.t("modifierSelectUiHandler:continueNextWaveDescription")); + ui.showText(i18next.t("rewardSelectUiHandler:continueNextWaveDescription")); return ret; } @@ -584,33 +587,34 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { ); } - const type = options[this.cursor].modifierTypeOption.type; - type && ui.showText(type.getDescription()); - if (type instanceof TmModifierType) { + const reward = options[this.cursor].rewardOption.type; + reward && ui.showText(reward.getDescription()); + if (isTmReward(reward)) { // prepare the move overlay to be shown with the toggle - this.moveInfoOverlay.show(allMoves[type.moveId]); + this.moveInfoOverlay.show(allMoves[reward.moveId]); } } else if (cursor === 0) { this.cursorObj.setPosition( 6, this.lockRarityButtonContainer.visible ? OPTION_BUTTON_YPOSITION - 8 : OPTION_BUTTON_YPOSITION + 4, ); - ui.showText(i18next.t("modifierSelectUiHandler:rerollDesc")); + ui.showText(i18next.t("rewardSelectUiHandler:rerollDesc")); } else if (cursor === 1) { this.cursorObj.setPosition( (globalScene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth) / 6 - 30, OPTION_BUTTON_YPOSITION + 4, ); - ui.showText(i18next.t("modifierSelectUiHandler:manageItemsDesc")); + // TODO @Wlowscha: Remember to rename these locales + ui.showText(i18next.t("rewardSelectUiHandler:manageItemsDesc")); } else if (cursor === 2) { this.cursorObj.setPosition( (globalScene.game.canvas.width - this.checkButtonWidth) / 6 - 10, OPTION_BUTTON_YPOSITION + 4, ); - ui.showText(i18next.t("modifierSelectUiHandler:checkTeamDesc")); + ui.showText(i18next.t("rewardSelectUiHandler:checkTeamDesc")); } else { this.cursorObj.setPosition(6, OPTION_BUTTON_YPOSITION + 4); - ui.showText(i18next.t("modifierSelectUiHandler:lockRaritiesDesc")); + ui.showText(i18next.t("rewardSelectUiHandler:lockRaritiesDesc")); } return ret; @@ -690,13 +694,13 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { const formattedMoney = formatMoney(globalScene.moneyFormat, this.rerollCost); - this.rerollCostText.setText(i18next.t("modifierSelectUiHandler:rerollCost", { formattedMoney })); + this.rerollCostText.setText(i18next.t("rewardSelectUiHandler:rerollCost", { formattedMoney })); this.rerollCostText.setColor(this.getTextColor(canReroll ? TextStyle.MONEY : TextStyle.PARTY_RED)); this.rerollCostText.setShadowColor(this.getTextColor(canReroll ? TextStyle.MONEY : TextStyle.PARTY_RED, true)); } updateLockRaritiesText(): void { - const textStyle = globalScene.lockModifierTiers ? TextStyle.SUMMARY_BLUE : TextStyle.PARTY; + const textStyle = globalScene.lockRarityTiers ? TextStyle.SUMMARY_BLUE : TextStyle.PARTY; this.lockRarityButtonText.setColor(this.getTextColor(textStyle)); this.lockRarityButtonText.setShadowColor(this.getTextColor(textStyle, true)); } @@ -720,7 +724,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { globalScene.hideLuckText(250); /* Normally already called just after the shop, but not sure if it happens in 100% of cases */ - globalScene.getModifierBar().updateModifiers(globalScene.modifiers); + globalScene.updateItems(true); const options = this.options.concat(this.shopOptionsRows.flat()); this.options.splice(0, this.options.length); @@ -772,7 +776,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { } class ModifierOption extends Phaser.GameObjects.Container { - public modifierTypeOption: ModifierTypeOption; + public rewardOption: RewardOption; private pb: Phaser.GameObjects.Sprite; private pbTint: Phaser.GameObjects.Sprite; private itemContainer: Phaser.GameObjects.Container; @@ -781,18 +785,18 @@ class ModifierOption extends Phaser.GameObjects.Container { private itemText: Phaser.GameObjects.Text; private itemCostText: Phaser.GameObjects.Text; - constructor(x: number, y: number, modifierTypeOption: ModifierTypeOption) { + constructor(x: number, y: number, rewardOption: RewardOption) { super(globalScene, x, y); - this.modifierTypeOption = modifierTypeOption; + this.rewardOption = rewardOption; this.setup(); } setup() { - if (!this.modifierTypeOption.cost) { + if (!this.rewardOption.cost) { const getPb = (): Phaser.GameObjects.Sprite => { - const pb = globalScene.add.sprite(0, -182, "pb", this.getPbAtlasKey(-this.modifierTypeOption.upgradeCount)); + const pb = globalScene.add.sprite(0, -182, "pb", this.getPbAtlasKey(-this.rewardOption.upgradeCount)); pb.setScale(2); return pb; }; @@ -811,28 +815,28 @@ class ModifierOption extends Phaser.GameObjects.Container { this.add(this.itemContainer); const getItem = () => { - const item = globalScene.add.sprite(0, 0, "items", this.modifierTypeOption.type?.iconImage); + const item = globalScene.add.sprite(0, 0, "items", this.rewardOption.type?.getIcon()); return item; }; this.item = getItem(); this.itemContainer.add(this.item); - if (!this.modifierTypeOption.cost) { + if (!this.rewardOption.cost) { this.itemTint = getItem(); this.itemTint.setTintFill(Phaser.Display.Color.GetColor(255, 192, 255)); this.itemContainer.add(this.itemTint); } - this.itemText = addTextObject(0, 35, this.modifierTypeOption.type?.name!, TextStyle.PARTY, { align: "center" }); // TODO: is this bang correct? + this.itemText = addTextObject(0, 35, this.rewardOption.type?.name!, TextStyle.PARTY, { align: "center" }); // TODO: is this bang correct? this.itemText.setOrigin(0.5, 0); this.itemText.setAlpha(0); this.itemText.setTint( - this.modifierTypeOption.type?.tier ? getModifierTierTextTint(this.modifierTypeOption.type?.tier) : undefined, + this.rewardOption.type?.tier ? getRarityTierTextTint(this.rewardOption.type?.tier) : undefined, ); this.add(this.itemText); - if (this.modifierTypeOption.cost) { + if (this.rewardOption.cost) { this.itemCostText = addTextObject(0, 45, "", TextStyle.MONEY, { align: "center", }); @@ -845,10 +849,12 @@ class ModifierOption extends Phaser.GameObjects.Container { } } + // TODO @SirzBenjie: Review to make sure I didn't bork the method + /** * Start the tweens responsible for animating the option's appearance * - * @privateremarks + * @privateRemarks * This method is unusual. It "returns" (one via the actual return, one by via appending to the `promiseHolder` * parameter) two promises. The promise returned by the method resolves once the option's appearance animations have * completed, and is meant to allow callers to synchronize with the completion of the option's appearance animations. @@ -914,12 +920,12 @@ class ModifierOption extends Phaser.GameObjects.Container { // TODO: Figure out proper delay between chains and then convert this into a single tween chain // rather than starting multiple tween chains. - for (let u = 0; u < this.modifierTypeOption.upgradeCount; u++) { + for (let u = 0; u < this.rewardOption.upgradeCount; u++) { const { resolve, promise } = Promise.withResolvers(); globalScene.tweens.chain({ tweens: [ { - delay: remainingDuration - 2000 * (this.modifierTypeOption.upgradeCount - (u + 1 + upgradeCountOffset)), + delay: remainingDuration - 2000 * (this.rewardOption.upgradeCount - (u + 1 + upgradeCountOffset)), onStart: () => { globalScene.playSound("se/upgrade", { rate: 1 + 0.25 * u, @@ -931,7 +937,7 @@ class ModifierOption extends Phaser.GameObjects.Container { duration: 1000, ease: "Sine.easeIn", onComplete: () => { - this.pb.setTexture("pb", this.getPbAtlasKey(-this.modifierTypeOption.upgradeCount + (u + 1))); + this.pb.setTexture("pb", this.getPbAtlasKey(-this.rewardOption.upgradeCount + (u + 1))); }, }, { @@ -1037,16 +1043,16 @@ class ModifierOption extends Phaser.GameObjects.Container { } getPbAtlasKey(tierOffset = 0) { - return getPokeballAtlasKey((this.modifierTypeOption.type?.tier! + tierOffset) as number as PokeballType); // TODO: is this bang correct? + return getPokeballAtlasKey((this.rewardOption.type?.tier! + tierOffset) as number as PokeballType); // TODO: is this bang correct? } updateCostText(): void { - const cost = Overrides.WAIVE_ROLL_FEE_OVERRIDE ? 0 : this.modifierTypeOption.cost; + const cost = Overrides.WAIVE_ROLL_FEE_OVERRIDE ? 0 : this.rewardOption.cost; const textStyle = cost <= globalScene.money ? TextStyle.MONEY : TextStyle.PARTY_RED; const formattedMoney = formatMoney(globalScene.moneyFormat, cost); - this.itemCostText.setText(i18next.t("modifierSelectUiHandler:itemCost", { formattedMoney })); + this.itemCostText.setText(i18next.t("rewardSelectUiHandler:itemCost", { formattedMoney })); this.itemCostText.setColor(getTextColor(textStyle, false, globalScene.uiTheme)); this.itemCostText.setShadowColor(getTextColor(textStyle, true, globalScene.uiTheme)); } diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 465e48a45ad..dbc7585f6e6 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -1,5 +1,6 @@ import { globalScene } from "#app/global-scene"; import { getBiomeName } from "#balance/biomes"; +import { allHeldItems, allTrainerItems } from "#data/data-lists"; import { getNatureName, getNatureStatMultiplier } from "#data/nature"; import { getPokeballAtlasKey } from "#data/pokeball"; import { getTypeRgb } from "#data/type"; @@ -15,9 +16,7 @@ import type { SpeciesId } from "#enums/species-id"; import { TextStyle } from "#enums/text-style"; import { TrainerVariant } from "#enums/trainer-variant"; import { UiMode } from "#enums/ui-mode"; -// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` -import * as Modifier from "#modifiers/modifier"; -import { getLuckString, getLuckTextTint } from "#modifiers/modifier-type"; +import { heldItemSortFunc } from "#items/item-utility"; import { getVariantTint } from "#sprites/variant"; import type { SessionSaveData } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; @@ -26,6 +25,7 @@ import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common"; +import { getLuckString, getLuckTextTint } from "#utils/party"; import i18next from "i18next"; import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle"; @@ -67,7 +67,6 @@ export class RunInfoUiHandler extends UiHandler { private endCardContainer: Phaser.GameObjects.Container; private partyVisibility: boolean; - private modifiersModule: any; constructor() { super(UiMode.RUN_INFO); @@ -75,8 +74,6 @@ export class RunInfoUiHandler extends UiHandler { override async setup() { this.runContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1); - // The import of the modifiersModule is loaded here to sidestep async/await issues. - this.modifiersModule = Modifier; this.runContainer.setVisible(false); globalScene.loadImage("encounter_exclaim", "mystery-encounters"); } @@ -177,7 +174,7 @@ export class RunInfoUiHandler extends UiHandler { const headerBg = addWindow(0, 0, globalScene.game.canvas.width / 6 - 2, 24); headerBg.setOrigin(0, 0); this.runContainer.add(headerBg); - if (this.runInfo.modifiers.length !== 0) { + if (this.runInfo.trainerItems.length !== 0) { const headerBgCoords = headerBg.getTopRight(); const abilityButtonContainer = globalScene.add.container(0, 0); const abilityButtonText = addTextObject(8, 0, i18next.t("runHistory:viewHeldItems"), TextStyle.WINDOW, { @@ -642,34 +639,31 @@ export class RunInfoUiHandler extends UiHandler { // Player Held Items // A max of 20 items can be displayed. A + sign will be added if the run's held items pushes past this maximum to show the user that there are more. - if (this.runInfo.modifiers.length) { - let visibleModifierIndex = 0; + if (this.runInfo.trainerItems.length) { + let visibleTrainerItemIndex = 0; - const modifierIconsContainer = globalScene.add.container( + const trainerItemIconsContainer = globalScene.add.container( 8, this.runInfo.gameMode === GameModes.CHALLENGE ? 20 : 15, ); - modifierIconsContainer.setScale(0.45); - for (const m of this.runInfo.modifiers) { - const modifier = m.toModifier(this.modifiersModule[m.className]); - if (modifier instanceof Modifier.PokemonHeldItemModifier) { - continue; - } - const icon = modifier?.getIcon(false); + trainerItemIconsContainer.setScale(0.45); + for (const m of this.runInfo.trainerItems) { + const itemId = m.id; + const icon = allTrainerItems[itemId].createIcon(m.stack); if (icon) { - const rowHeightModifier = Math.floor(visibleModifierIndex / 7); - icon.setPosition(24 * (visibleModifierIndex % 7), 20 + 35 * rowHeightModifier); - modifierIconsContainer.add(icon); + const rowHeightTrainerItem = Math.floor(visibleTrainerItemIndex / 7); + icon.setPosition(24 * (visibleTrainerItemIndex % 7), 20 + 35 * rowHeightTrainerItem); + trainerItemIconsContainer.add(icon); } - if (++visibleModifierIndex === 20) { + if (++visibleTrainerItemIndex === 20) { const maxItems = addTextObject(45, 90, "+", TextStyle.WINDOW); - maxItems.setPositionRelative(modifierIconsContainer, 70, 45); + maxItems.setPositionRelative(trainerItemIconsContainer, 70, 45); this.runInfoContainer.add(maxItems); break; } } - this.runInfoContainer.add(modifierIconsContainer); + this.runInfoContainer.add(trainerItemIconsContainer); } this.runInfoContainer.add(modeText); @@ -885,38 +879,22 @@ export class RunInfoUiHandler extends UiHandler { const heldItemsScale = this.runInfo.gameMode === GameModes.SPLICED_ENDLESS || this.runInfo.gameMode === GameModes.ENDLESS ? 0.25 : 0.5; const heldItemsContainer = globalScene.add.container(-82, 2); - const heldItemsList: Modifier.PokemonHeldItemModifier[] = []; - if (this.runInfo.modifiers.length) { - for (const m of this.runInfo.modifiers) { - const modifier = m.toModifier(this.modifiersModule[m.className]); - if (modifier instanceof Modifier.PokemonHeldItemModifier && modifier.pokemonId === pokemon.id) { - modifier.stackCount = m["stackCount"]; - heldItemsList.push(modifier); - } + let row = 0; + for (const [index, item] of pokemon.heldItemManager.getHeldItems().sort(heldItemSortFunc).entries()) { + if (index > 36) { + const overflowIcon = addTextObject(182, 4, "+", TextStyle.WINDOW); + heldItemsContainer.add(overflowIcon); + break; } - if (heldItemsList.length > 0) { - (heldItemsList as Modifier.PokemonHeldItemModifier[]).sort(Modifier.modifierSortFunc); - let row = 0; - for (const [index, item] of heldItemsList.entries()) { - if (index > 36) { - const overflowIcon = addTextObject(182, 4, "+", TextStyle.WINDOW); - heldItemsContainer.add(overflowIcon); - break; - } - const itemIcon = item?.getIcon(true); - if ( - item?.stackCount < item?.getMaxHeldItemCount(pokemon) && - itemIcon.list[1] instanceof Phaser.GameObjects.BitmapText - ) { - itemIcon.list[1].clearTint(); - } - itemIcon.setScale(heldItemsScale); - itemIcon.setPosition((index % 19) * 10, row * 10); - heldItemsContainer.add(itemIcon); - if (index !== 0 && index % 18 === 0) { - row++; - } - } + const itemIcon = allHeldItems[item].createSummaryIcon(pokemon); + if (!pokemon.heldItemManager.isMaxStack(item) && itemIcon.list[1] instanceof Phaser.GameObjects.BitmapText) { + itemIcon.list[1].clearTint(); + } + itemIcon.setScale(heldItemsScale); + itemIcon.setPosition((index % 19) * 10, row * 10); + heldItemsContainer.add(itemIcon); + if (index !== 0 && index % 18 === 0) { + row++; } } heldItemsContainer.setName("heldItems"); @@ -1143,7 +1121,7 @@ export class RunInfoUiHandler extends UiHandler { } break; case Button.CYCLE_ABILITY: - if (this.runInfo.modifiers.length !== 0 && this.pageMode === RunInfoUiMode.MAIN) { + if (this.runInfo.trainerItems.length !== 0 && this.pageMode === RunInfoUiMode.MAIN) { if (this.partyVisibility) { this.showParty(false); } else { diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 9da34e672f1..2f53411c142 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -1,10 +1,9 @@ import { GameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; +import { allTrainerItems } from "#data/data-lists"; import { Button } from "#enums/buttons"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` -import * as Modifier from "#modifiers/modifier"; import type { SessionSaveData } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; import { MessageUiHandler } from "#ui/message-ui-handler"; @@ -444,12 +443,8 @@ class SessionSlot extends Phaser.GameObjects.Container { const modifierIconsContainer = globalScene.add.container(148, 30); modifierIconsContainer.setScale(0.5); let visibleModifierIndex = 0; - for (const m of data.modifiers) { - const modifier = m.toModifier(Modifier[m.className]); - if (modifier instanceof Modifier.PokemonHeldItemModifier) { - continue; - } - const icon = modifier?.getIcon(false); + for (const m of data.trainerItems) { + const icon = allTrainerItems[m.id].createIcon(m.stack); if (icon) { icon.setPosition(24 * visibleModifierIndex, 0); modifierIconsContainer.add(icon); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index b51bdfdb157..ddfcb6ba5d5 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -4,6 +4,7 @@ import { globalScene } from "#app/global-scene"; import { starterColors } from "#app/global-vars/starter-colors"; import { getBiomeName } from "#balance/biomes"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#balance/starters"; +import { allHeldItems } from "#data/data-lists"; import { getLevelRelExp, getLevelTotalExp } from "#data/exp"; import { getGenderColor, getGenderSymbol } from "#data/gender"; import { getNatureName, getNatureStatMultiplier } from "#data/nature"; @@ -19,7 +20,7 @@ import { StatusEffect } from "#enums/status-effect"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; -import { modifierSortFunc, PokemonHeldItemModifier } from "#modifiers/modifier"; +import { heldItemSortFunc } from "#items/item-utility"; import type { Move } from "#moves/move"; import type { PokemonMove } from "#moves/pokemon-move"; import type { Variant } from "#sprites/variant"; @@ -1035,24 +1036,39 @@ export class SummaryUiHandler extends UiHandler { }); this.ivContainer.setVisible(false); - const itemModifiers = ( - globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.pokemon?.id, - this.playerParty, - ) as PokemonHeldItemModifier[] - ).sort(modifierSortFunc); + const heldItems = this.pokemon?.getHeldItems().sort(heldItemSortFunc); - itemModifiers.forEach((item, i) => { - const icon = item.getIcon(true); + heldItems?.forEach((itemKey, i) => { + const heldItem = allHeldItems[itemKey]; + if (this.pokemon) { + const icon = heldItem.createSummaryIcon(this.pokemon); + + console.log(icon); + icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); + this.statsContainer.add(icon); + + icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); + icon.on("pointerover", () => globalScene.ui.showTooltip(heldItem.name, heldItem.description, true)); + icon.on("pointerout", () => globalScene.ui.hideTooltip()); + } + }); + /* + const formChangeItems = this.pokemon?.heldItemManager.getFormChangeItems().sort(formChangeItemSortFunc); + + //TODO: Make an equivalent function for form change items + formChangeItems?.forEach((itemKey, i) => { + const icon = heldItem.createSummaryIcon(stack); + + console.log(icon); icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); this.statsContainer.add(icon); icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); - icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true)); + icon.on("pointerover", () => globalScene.ui.showTooltip(heldItem.getName(), heldItem.getDescription(), true)); icon.on("pointerout", () => globalScene.ui.hideTooltip()); }); - +*/ const pkmLvl = this.pokemon?.level!; // TODO: is this bang correct? const pkmLvlExp = this.pokemon?.levelExp!; // TODO: is this bang correct? const pkmExp = this.pokemon?.exp!; // TODO: is this bang correct? diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index 5ea89929cda..2603630f351 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -5,10 +5,10 @@ import { Button } from "#enums/buttons"; import type { MoveId } from "#enums/move-id"; import { UiMode } from "#enums/ui-mode"; import type { Pokemon } from "#field/pokemon"; -import type { ModifierBar } from "#modifiers/modifier"; import { getMoveTargets } from "#moves/move-utils"; import { UiHandler } from "#ui/ui-handler"; import { fixedInt, isNullOrUndefined } from "#utils/common"; +import type { ItemBar } from "./item-bar-ui"; export type TargetSelectCallback = (targets: BattlerIndex[]) => void; @@ -23,7 +23,7 @@ export class TargetSelectUiHandler extends UiHandler { private targets: BattlerIndex[]; private targetsHighlighted: Pokemon[]; private targetFlashTween: Phaser.Tweens.Tween | null; - private enemyModifiers: ModifierBar; + private enemyModifiers: ItemBar; private targetBattleInfoMoveTween: Phaser.Tweens.Tween[] = []; constructor() { @@ -54,7 +54,7 @@ export class TargetSelectUiHandler extends UiHandler { return false; } - this.enemyModifiers = globalScene.getModifierBar(true); + this.enemyModifiers = globalScene.getItemBar(true); if (this.fieldIndex === BattlerIndex.PLAYER) { this.resetCursor(this.cursor0, user); diff --git a/src/ui/text.ts b/src/ui/text.ts index 8aa50983874..f44fd79b6ff 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import { EggTier } from "#enums/egg-type"; -import { ModifierTier } from "#enums/modifier-tier"; +import { RarityTier } from "#enums/reward-tier"; import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import i18next from "#plugins/i18n"; @@ -643,19 +643,19 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui } } -export function getModifierTierTextTint(tier: ModifierTier): number { +export function getRarityTierTextTint(tier: RarityTier): number { switch (tier) { - case ModifierTier.COMMON: + case RarityTier.COMMON: return 0xf8f8f8; - case ModifierTier.GREAT: + case RarityTier.GREAT: return 0x4998f8; - case ModifierTier.ULTRA: + case RarityTier.ULTRA: return 0xf8d038; - case ModifierTier.ROGUE: + case RarityTier.ROGUE: return 0xdb4343; - case ModifierTier.MASTER: + case RarityTier.MASTER: return 0xe331c5; - case ModifierTier.LUXURY: + case RarityTier.LUXURY: return 0xe74c18; } } @@ -663,12 +663,12 @@ export function getModifierTierTextTint(tier: ModifierTier): number { export function getEggTierTextTint(tier: EggTier): number { switch (tier) { case EggTier.COMMON: - return getModifierTierTextTint(ModifierTier.COMMON); + return getRarityTierTextTint(RarityTier.COMMON); case EggTier.RARE: - return getModifierTierTextTint(ModifierTier.GREAT); + return getRarityTierTextTint(RarityTier.GREAT); case EggTier.EPIC: - return getModifierTierTextTint(ModifierTier.ULTRA); + return getRarityTierTextTint(RarityTier.ULTRA); case EggTier.LEGENDARY: - return getModifierTierTextTint(ModifierTier.MASTER); + return getRarityTierTextTint(RarityTier.MASTER); } } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 4c8f0613122..c3a0ff365ea 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -28,7 +28,6 @@ import { LoadingModalUiHandler } from "#ui/loading-modal-ui-handler"; import { LoginFormUiHandler } from "#ui/login-form-ui-handler"; import { MenuUiHandler } from "#ui/menu-ui-handler"; import { MessageUiHandler } from "#ui/message-ui-handler"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler"; import { NavigationManager } from "#ui/navigation-menu"; import { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; @@ -38,6 +37,7 @@ import { PokedexScanUiHandler } from "#ui/pokedex-scan-ui-handler"; import { PokedexUiHandler } from "#ui/pokedex-ui-handler"; import { RegistrationFormUiHandler } from "#ui/registration-form-ui-handler"; import { RenameFormUiHandler } from "#ui/rename-form-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { RunHistoryUiHandler } from "#ui/run-history-ui-handler"; import { RunInfoUiHandler } from "#ui/run-info-ui-handler"; import { SaveSlotSelectUiHandler } from "#ui/save-slot-select-ui-handler"; @@ -132,7 +132,7 @@ export class UI extends Phaser.GameObjects.Container { new FightUiHandler(), new BallUiHandler(), new TargetSelectUiHandler(), - new ModifierSelectUiHandler(), + new RewardSelectUiHandler(), new SaveSlotSelectUiHandler(), new PartyUiHandler(), new SummaryUiHandler(), diff --git a/src/utils/common.ts b/src/utils/common.ts index 1c75dac93b4..1730a1ce82b 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -267,6 +267,14 @@ export function formatStat(stat: number, forHp = false): string { return formatLargeNumber(stat, forHp ? 100_000 : 1_000_000); } +export function getTypedKeys, K extends number = Extract>(obj: T): K[] { + return Object.keys(obj).map(k => Number(k) as K); +} + +export function getTypedEntries(obj: T): [keyof T, T[keyof T]][] { + return Object.entries(obj) as [keyof T, T[keyof T]][]; +} + export function executeIf(condition: boolean, promiseFunc: () => Promise): Promise { return condition ? promiseFunc() : new Promise(resolve => resolve(null)); } @@ -538,3 +546,22 @@ export function coerceArray(input: T): T extends any[] ? T : [T]; export function coerceArray(input: T): T | [T] { return Array.isArray(input) ? input : [input]; } + +export function pickWeightedIndex(weights: number[]): number | undefined { + const totalWeight = weights.reduce((sum, w) => sum + w, 0); + + if (totalWeight <= 0) { + return undefined; + } + + let r = randSeedFloat() * totalWeight; + + for (let i = 0; i < weights.length; i++) { + if (r < weights[i]) { + return i; + } + r -= weights[i]; + } + + return undefined; // TODO: Change to something more appropriate +} diff --git a/src/utils/modifier-utils.ts b/src/utils/modifier-utils.ts deleted file mode 100644 index 58108e7a599..00000000000 --- a/src/utils/modifier-utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { modifierTypes } from "#data/data-lists"; -import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { - dailyStarterModifierPool, - enemyBuffModifierPool, - modifierPool, - trainerModifierPool, - wildModifierPool, -} from "#modifiers/modifier-pools"; -import type { ModifierType } from "#modifiers/modifier-type"; -import type { ModifierPool, ModifierTypeFunc } from "#types/modifier-types"; - -export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool { - switch (poolType) { - case ModifierPoolType.PLAYER: - return modifierPool; - case ModifierPoolType.WILD: - return wildModifierPool; - case ModifierPoolType.TRAINER: - return trainerModifierPool; - case ModifierPoolType.ENEMY_BUFF: - return enemyBuffModifierPool; - case ModifierPoolType.DAILY_STARTER: - return dailyStarterModifierPool; - } -} - -// TODO: document this -export function getModifierType(modifierTypeFunc: ModifierTypeFunc): ModifierType { - const modifierType = modifierTypeFunc(); - if (!modifierType.id) { - modifierType.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifierTypeFunc)!; // TODO: is this bang correct? - } - return modifierType; -} diff --git a/src/utils/party.ts b/src/utils/party.ts new file mode 100644 index 00000000000..f1ed8579f29 --- /dev/null +++ b/src/utils/party.ts @@ -0,0 +1,56 @@ +import { timedEventManager } from "#app/global-event-manager"; +import { globalScene } from "#app/global-scene"; +import { RarityTier } from "#enums/reward-tier"; +import type { Pokemon } from "#field/pokemon"; +import { getRarityTierTextTint } from "#ui/text"; +import { NumberHolder, randSeedInt } from "./common"; + +/** + * Calculates the team's luck value. + * @param party The player's party. + * @returns A number between 0 and 14 based on the party's total luck value, or a random number between 0 and 14 if the player is in Daily Run mode. + */ +export function getPartyLuckValue(party: Pokemon[]): number { + if (globalScene.gameMode.isDaily) { + const DailyLuck = new NumberHolder(0); + globalScene.executeWithSeedOffset( + () => { + DailyLuck.value = randSeedInt(15); // Random number between 0 and 14 + }, + 0, + globalScene.seed, + ); + return DailyLuck.value; + } + const eventSpecies = timedEventManager.getEventLuckBoostedSpecies(); + const luck = Phaser.Math.Clamp( + party + .map(p => (p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0)) + .reduce((total: number, value: number) => (total += value), 0), + 0, + 14, + ); + return Math.min(timedEventManager.getEventLuckBoost() + (luck ?? 0), 14); +} + +export function getLuckString(luckValue: number): string { + return ["D", "C", "C+", "B-", "B", "B+", "A-", "A", "A+", "A++", "S", "S+", "SS", "SS+", "SSS"][luckValue]; +} + +export function getLuckTextTint(luckValue: number): number { + let rarityTier: RarityTier; + if (luckValue > 11) { + rarityTier = RarityTier.LUXURY; + } else if (luckValue > 9) { + rarityTier = RarityTier.MASTER; + } else if (luckValue > 5) { + rarityTier = RarityTier.ROGUE; + } else if (luckValue > 2) { + rarityTier = RarityTier.ULTRA; + } else if (luckValue) { + rarityTier = RarityTier.GREAT; + } else { + rarityTier = RarityTier.COMMON; + } + return getRarityTierTextTint(rarityTier); +} diff --git a/test/abilities/ability-activation-order.test.ts b/test/abilities/ability-activation-order.test.ts index 54ff5e8b99a..f77fd0be613 100644 --- a/test/abilities/ability-activation-order.test.ts +++ b/test/abilities/ability-activation-order.test.ts @@ -1,7 +1,7 @@ import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { Stat } from "#enums/stat"; import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; @@ -48,7 +48,7 @@ describe("Ability Activation Order", () => { .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.DROUGHT) .ability(AbilityId.DRIZZLE) - .startingHeldItems([{ name: "BASE_STAT_BOOSTER", type: Stat.SPD, count: 100 }]); + .startingHeldItems([{ entry: HeldItemId.CARBOS, count: 100 }]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY); @@ -61,7 +61,7 @@ describe("Ability Activation Order", () => { .enemySpecies(SpeciesId.DITTO) .enemyAbility(AbilityId.DROUGHT) .ability(AbilityId.DRIZZLE) - .startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]); + .startingHeldItems([{ entry: HeldItemId.QUICK_POWDER }]); await game.classicMode.startBattle([SpeciesId.DITTO]); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY); diff --git a/test/abilities/arena-trap.test.ts b/test/abilities/arena-trap.test.ts index f85fae5b259..48fa1ddd9f3 100644 --- a/test/abilities/arena-trap.test.ts +++ b/test/abilities/arena-trap.test.ts @@ -2,6 +2,7 @@ import { allAbilities } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -46,7 +47,7 @@ describe("Abilities - Arena Trap", () => { }); it("should guarantee double battle with any one LURE", async () => { - game.override.startingModifier([{ name: "LURE" }]).startingWave(2); + game.override.startingTrainerItems([{ entry: TrainerItemId.LURE }]).startingWave(2); await game.classicMode.startBattle(); diff --git a/test/abilities/competitive.test.ts b/test/abilities/competitive.test.ts index 1a083705e5c..f1e5164fa3c 100644 --- a/test/abilities/competitive.test.ts +++ b/test/abilities/competitive.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -59,7 +60,7 @@ describe("Abilities - Competitive", () => { }); it("white herb should remove only the negative effects", async () => { - game.override.startingHeldItems([{ name: "WHITE_HERB" }]); + game.override.startingHeldItems([{ entry: HeldItemId.WHITE_HERB }]); await game.classicMode.startBattle([SpeciesId.FLYGON]); const playerPokemon = game.scene.getPlayerPokemon()!; diff --git a/test/abilities/cud-chew.test.ts b/test/abilities/cud-chew.test.ts index 5f15de04cae..50d6f3dd1fa 100644 --- a/test/abilities/cud-chew.test.ts +++ b/test/abilities/cud-chew.test.ts @@ -3,6 +3,7 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { AbilityId } from "#enums/ability-id"; import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -30,7 +31,7 @@ describe("Abilities - Cud Chew", () => { game = new GameManager(phaserGame); game.override .moveset([MoveId.BUG_BITE, MoveId.SPLASH, MoveId.HYPER_VOICE, MoveId.STUFF_CHEEKS]) - .startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]) + .startingHeldItems([{ entry: HeldItemId.SITRUS_BERRY }]) .ability(AbilityId.CUD_CHEW) .battleStyle("single") .criticalHits(false) @@ -114,8 +115,8 @@ describe("Abilities - Cud Chew", () => { vi.spyOn(Pokemon.prototype, "randBattleSeedInt").mockReturnValue(0); game.override .startingHeldItems([ - { name: "BERRY", type: BerryType.PETAYA, count: 3 }, - { name: "BERRY", type: BerryType.LIECHI, count: 3 }, + { entry: HeldItemId.PETAYA_BERRY, count: 3 }, + { entry: HeldItemId.LIECHI_BERRY, count: 3 }, ]) .enemyMoveset(MoveId.TEATIME); await game.classicMode.startBattle([SpeciesId.FARIGIRAF]); @@ -258,7 +259,7 @@ describe("Abilities - Cud Chew", () => { it("works with pluck", async () => { game.override .enemySpecies(SpeciesId.BLAZIKEN) - .enemyHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]) + .enemyHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) .startingHeldItems([]); await game.classicMode.startBattle([SpeciesId.FARIGIRAF]); diff --git a/test/abilities/defiant.test.ts b/test/abilities/defiant.test.ts index 29c386ff093..363198fbbe8 100644 --- a/test/abilities/defiant.test.ts +++ b/test/abilities/defiant.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -58,7 +59,7 @@ describe("Abilities - Defiant", () => { }); it("white herb should remove only the negative effects", async () => { - game.override.startingHeldItems([{ name: "WHITE_HERB" }]); + game.override.startingHeldItems([{ entry: HeldItemId.WHITE_HERB }]); await game.classicMode.startBattle([SpeciesId.FLYGON]); const playerPokemon = game.scene.getPlayerPokemon()!; diff --git a/test/abilities/flower-veil.test.ts b/test/abilities/flower-veil.test.ts index 46478a3dd9c..6da68fe82d4 100644 --- a/test/abilities/flower-veil.test.ts +++ b/test/abilities/flower-veil.test.ts @@ -2,6 +2,7 @@ import { allAbilities, allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -44,7 +45,7 @@ describe("Abilities - Flower Veil", () => { game.override .enemyMoveset([MoveId.TACKLE, MoveId.SPLASH]) .moveset([MoveId.REST, MoveId.SPLASH]) - .startingHeldItems([{ name: "FLAME_ORB" }]); + .startingHeldItems([{ entry: HeldItemId.FLAME_ORB }]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const user = game.scene.getPlayerPokemon()!; game.move.select(MoveId.REST); diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts index d27ee491fed..82f964c9d0f 100644 --- a/test/abilities/harvest.test.ts +++ b/test/abilities/harvest.test.ts @@ -2,15 +2,15 @@ import { PostTurnRestoreBerryAbAttr } from "#abilities/ability"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { WeatherType } from "#enums/weather-type"; -import type { Pokemon } from "#field/pokemon"; -import { BerryModifier, PreserveBerryModifier } from "#modifiers/modifier"; -import type { ModifierOverride } from "#modifiers/modifier-type"; +import type { PokemonItemMap } from "#items/held-item-data-types"; +import { getPartyBerries } from "#items/item-utility"; import { GameManager } from "#test/test-utils/game-manager"; -import type { BooleanHolder } from "#utils/common"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -18,15 +18,9 @@ describe("Abilities - Harvest", () => { let phaserGame: Phaser.Game; let game: GameManager; - const getPlayerBerries = () => - game.scene.getModifiers(BerryModifier, true).filter(b => b.pokemonId === game.scene.getPlayerPokemon()?.id); - /** Check whether the player's Modifiers contains the specified berries and nothing else. */ - function expectBerriesContaining(...berries: ModifierOverride[]): void { - const actualBerries: ModifierOverride[] = getPlayerBerries().map( - // only grab berry type and quantity since that's literally all we care about - b => ({ name: "BERRY", type: b.berryType, count: b.getStackCount() }), - ); + function expectBerriesContaining(berries: PokemonItemMap[]): void { + const actualBerries = getPartyBerries(); expect(actualBerries).toEqual(berries); } @@ -57,17 +51,19 @@ describe("Abilities - Harvest", () => { }); it("replenishes eaten berries", async () => { - game.override.startingHeldItems([{ name: "BERRY", type: BerryType.LUM, count: 1 }]); + game.override.startingHeldItems([{ entry: HeldItemId.LUM_BERRY }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); await game.move.selectEnemyMove(MoveId.NUZZLE); await game.phaseInterceptor.to("BerryPhase"); - expect(getPlayerBerries()).toHaveLength(0); + expect(getPartyBerries()).toHaveLength(0); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(1); await game.phaseInterceptor.to("TurnEndPhase"); - expectBerriesContaining({ name: "BERRY", type: BerryType.LUM, count: 1 }); + expectBerriesContaining([ + { item: { id: HeldItemId.LUM_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()?.id! }, + ]); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); }); @@ -76,8 +72,8 @@ describe("Abilities - Harvest", () => { // the game consider all other pokemon to *not* have their respective abilities. game.override .startingHeldItems([ - { name: "BERRY", type: BerryType.ENIGMA, count: 2 }, - { name: "BERRY", type: BerryType.LUM, count: 2 }, + { entry: HeldItemId.ENIGMA_BERRY, count: 2 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ]) .enemyAbility(AbilityId.NEUTRALIZING_GAS); await game.classicMode.startBattle([SpeciesId.MILOTIC]); @@ -91,7 +87,7 @@ describe("Abilities - Harvest", () => { await game.toNextTurn(); expect(milotic.battleData.berriesEaten).toEqual(expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM])); - expect(getPlayerBerries()).toHaveLength(2); + expect(getPartyBerries()).toHaveLength(2); // Give ourselves harvest and disable enemy neut gas, // but force our roll to fail so we don't accidentally recover anything @@ -105,7 +101,7 @@ describe("Abilities - Harvest", () => { expect(milotic.battleData.berriesEaten).toEqual( expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM, BerryType.ENIGMA, BerryType.LUM]), ); - expect(getPlayerBerries()).toHaveLength(0); + expect(getPartyBerries()).toHaveLength(0); // proc a high roll and we _should_ get a berry back! game.move.select(MoveId.SPLASH); @@ -113,13 +109,12 @@ describe("Abilities - Harvest", () => { await game.toNextTurn(); expect(milotic.battleData.berriesEaten).toHaveLength(3); - expect(getPlayerBerries()).toHaveLength(1); + expect(getPartyBerries()).toHaveLength(1); }); it("remembers berries eaten array across waves", async () => { - game.override - .startingHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 2 }]) - .ability(AbilityId.BALL_FETCH); // don't actually need harvest for this test + game.override; + game.override.startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY, count: 2 }]).ability(AbilityId.BALL_FETCH); // don't actually need harvest for this test await game.classicMode.startBattle([SpeciesId.REGIELEKI]); const regieleki = game.scene.getPlayerPokemon()!; @@ -132,19 +127,19 @@ describe("Abilities - Harvest", () => { // ate 1 berry without recovering (no harvest) expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]); - expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expectBerriesContaining([{ item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: regieleki.id }]); expect(regieleki.getStatStage(Stat.SPATK)).toBe(1); await game.toNextWave(); expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]); - expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expectBerriesContaining([{ item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: regieleki.id }]); expect(regieleki.getStatStage(Stat.SPATK)).toBe(1); }); it("keeps harvested berries across reloads", async () => { game.override - .startingHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]) + .startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) .moveset([MoveId.SPLASH, MoveId.EARTHQUAKE]) .enemyMoveset([MoveId.SUPER_FANG, MoveId.HEAL_PULSE]) .enemyAbility(AbilityId.COMPOUND_EYES); @@ -160,7 +155,9 @@ describe("Abilities - Harvest", () => { // ate 1 berry and recovered it expect(regieleki.battleData.berriesEaten).toEqual([]); - expect(getPlayerBerries()).toEqual([expect.objectContaining({ berryType: BerryType.PETAYA, stackCount: 1 })]); + expectBerriesContaining([ + { item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()?.id! }, + ]); expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); // heal up so harvest doesn't proc and kill enemy @@ -169,22 +166,25 @@ describe("Abilities - Harvest", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); - expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expectBerriesContaining([ + { item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()?.id! }, + ]); expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); await game.reload.reloadSession(); expect(regieleki.battleData.berriesEaten).toEqual([]); - expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expectBerriesContaining([ + { item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()?.id! }, + ]); expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); }); it("cannot restore capped berries", async () => { - const initBerries: ModifierOverride[] = [ - { name: "BERRY", type: BerryType.LUM, count: 2 }, - { name: "BERRY", type: BerryType.STARF, count: 2 }, - ]; - game.override.startingHeldItems(initBerries); + game.override.startingHeldItems([ + { entry: HeldItemId.LUM_BERRY, count: 2 }, + { entry: HeldItemId.STARF_BERRY, count: 2 }, + ]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); const feebas = game.scene.getPlayerPokemon()!; @@ -201,16 +201,16 @@ describe("Abilities - Harvest", () => { await game.phaseInterceptor.to("TurnEndPhase"); // recovered a starf - expectBerriesContaining( - { name: "BERRY", type: BerryType.LUM, count: 2 }, - { name: "BERRY", type: BerryType.STARF, count: 3 }, - ); + expectBerriesContaining([ + { item: { id: HeldItemId.LUM_BERRY, stack: 2 }, pokemonId: feebas.id }, + { item: { id: HeldItemId.STARF_BERRY, stack: 3 }, pokemonId: feebas.id }, + ]); }); it("does nothing if all berries are capped", async () => { - const initBerries: ModifierOverride[] = [ - { name: "BERRY", type: BerryType.LUM, count: 2 }, - { name: "BERRY", type: BerryType.STARF, count: 3 }, + const initBerries = [ + { entry: HeldItemId.LUM_BERRY, count: 2 }, + { entry: HeldItemId.STARF_BERRY, count: 3 }, ]; game.override.startingHeldItems(initBerries); await game.classicMode.startBattle([SpeciesId.FEEBAS]); @@ -222,12 +222,15 @@ describe("Abilities - Harvest", () => { await game.move.selectEnemyMove(MoveId.SPLASH); await game.phaseInterceptor.to("TurnEndPhase"); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.LUM_BERRY, stack: 2 }, pokemonId: player.id }, + { item: { id: HeldItemId.STARF_BERRY, stack: 3 }, pokemonId: player.id }, + ]); }); describe("move/ability interactions", () => { it("cannot restore incinerated berries", async () => { - game.override.startingHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 3 }]); + game.override.startingHeldItems([{ entry: HeldItemId.STARF_BERRY, count: 3 }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -238,7 +241,7 @@ describe("Abilities - Harvest", () => { }); it("cannot restore knocked off berries", async () => { - game.override.startingHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 3 }]); + game.override.startingHeldItems([{ entry: HeldItemId.STARF_BERRY, count: 3 }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -249,8 +252,7 @@ describe("Abilities - Harvest", () => { }); it("can restore berries eaten by Teatime", async () => { - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.STARF, count: 1 }]; - game.override.startingHeldItems(initBerries).enemyMoveset(MoveId.TEATIME); + game.override.startingHeldItems([{ entry: HeldItemId.STARF_BERRY }]).enemyMoveset(MoveId.TEATIME); await game.classicMode.startBattle([SpeciesId.FEEBAS]); // nom nom the berr berr yay yay @@ -258,12 +260,16 @@ describe("Abilities - Harvest", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.STARF_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()!.id }, + ]); }); it("cannot restore Plucked berries for either side", async () => { - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]; - game.override.startingHeldItems(initBerries).enemyAbility(AbilityId.HARVEST).enemyMoveset(MoveId.PLUCK); + game.override + .startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) + .enemyAbility(AbilityId.HARVEST) + .enemyMoveset(MoveId.PLUCK); await game.classicMode.startBattle([SpeciesId.FEEBAS]); // gobble gobble gobble @@ -273,20 +279,13 @@ describe("Abilities - Harvest", () => { // pluck triggers harvest for neither side expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); expect(game.scene.getEnemyPokemon()?.battleData.berriesEaten).toEqual([]); - expect(getPlayerBerries()).toEqual([]); + expect(getPartyBerries()).toEqual([]); }); it("cannot restore berries preserved via Berry Pouch", async () => { - // mock berry pouch to have a 100% success rate - vi.spyOn(PreserveBerryModifier.prototype, "apply").mockImplementation( - (_pokemon: Pokemon, doPreserve: BooleanHolder): boolean => { - doPreserve.value = false; - return true; - }, - ); - - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]; - game.override.startingHeldItems(initBerries).startingModifier([{ name: "BERRY_POUCH", count: 1 }]); + game.override + .startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) + .startingTrainerItems([{ entry: TrainerItemId.BERRY_POUCH, count: 5850 }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -294,11 +293,13 @@ describe("Abilities - Harvest", () => { // won't trigger harvest since we didn't lose the berry (it just doesn't ever add it to the array) expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()!.id }, + ]); }); it("can restore stolen berries", async () => { - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]; + const initBerries = [{ entry: HeldItemId.SITRUS_BERRY }]; game.override.enemyHeldItems(initBerries).passiveAbility(AbilityId.MAGICIAN).hasPassiveAbility(true); await game.classicMode.startBattle([SpeciesId.MEOWSCARADA]); @@ -315,32 +316,35 @@ describe("Abilities - Harvest", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(player.battleData.berriesEaten).toEqual([]); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.SITRUS_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()!.id }, + ]); }); // TODO: Enable once fling actually works...??? it.todo("can restore berries flung at user", async () => { - game.override.enemyHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 1 }]).enemyMoveset(MoveId.FLING); + game.override.enemyHeldItems([{ entry: HeldItemId.STARF_BERRY }]).enemyMoveset(MoveId.FLING); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toBe([]); - expect(getPlayerBerries()).toEqual([]); + expect(getPartyBerries()).toEqual([]); }); // TODO: Enable once Nat Gift gets implemented...??? it.todo("can restore berries consumed via Natural Gift", async () => { - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.STARF, count: 1 }]; - game.override.startingHeldItems(initBerries); + game.override.startingHeldItems([{ entry: HeldItemId.STARF_BERRY }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.NATURAL_GIFT); await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(0); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.STARF_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()!.id }, + ]); }); }); }); diff --git a/test/abilities/illuminate.test.ts b/test/abilities/illuminate.test.ts index 814b7dc64b7..0d7c20b509a 100644 --- a/test/abilities/illuminate.test.ts +++ b/test/abilities/illuminate.test.ts @@ -1,6 +1,7 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { Stat } from "#enums/stat"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -45,7 +46,7 @@ describe("Abilities - Illuminate", () => { }); it("should guarantee double battle with any one LURE", async () => { - game.override.startingModifier([{ name: "LURE" }]).startingWave(2); + game.override.startingTrainerItems([{ entry: TrainerItemId.LURE }]).startingWave(2); await game.classicMode.startBattle(); diff --git a/test/abilities/illusion.test.ts b/test/abilities/illusion.test.ts index e48cd9e9b78..33e2734c637 100644 --- a/test/abilities/illusion.test.ts +++ b/test/abilities/illusion.test.ts @@ -1,5 +1,6 @@ import { Gender } from "#data/gender"; import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { PokeballType } from "#enums/pokeball"; import { SpeciesId } from "#enums/species-id"; @@ -28,9 +29,9 @@ describe("Abilities - Illusion", () => { .enemySpecies(SpeciesId.ZORUA) .enemyAbility(AbilityId.ILLUSION) .enemyMoveset(MoveId.TACKLE) - .enemyHeldItems([{ name: "WIDE_LENS", count: 3 }]) + .enemyHeldItems([{ entry: HeldItemId.WIDE_LENS, count: 3 }]) .moveset([MoveId.WORRY_SEED, MoveId.SOAK, MoveId.TACKLE]) - .startingHeldItems([{ name: "WIDE_LENS", count: 3 }]); + .startingHeldItems([{ entry: HeldItemId.WIDE_LENS, count: 3 }]); }); it("creates illusion at the start", async () => { diff --git a/test/abilities/no-guard.test.ts b/test/abilities/no-guard.test.ts index 9ce12e710e5..7fb8a6837d7 100644 --- a/test/abilities/no-guard.test.ts +++ b/test/abilities/no-guard.test.ts @@ -3,6 +3,7 @@ import { BattlerIndex } from "#enums/battler-index"; import { HitCheckResult } from "#enums/hit-check-result"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { MoveEffectPhase } from "#phases/move-effect-phase"; import { MoveEndPhase } from "#phases/move-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; @@ -54,7 +55,7 @@ describe("Abilities - No Guard", () => { }); it("should guarantee double battle with any one LURE", async () => { - game.override.startingModifier([{ name: "LURE" }]).startingWave(2); + game.override.startingTrainerItems([{ entry: TrainerItemId.LURE }]).startingWave(2); await game.classicMode.startBattle(); diff --git a/test/abilities/normal-move-type-change.test.ts b/test/abilities/normal-move-type-change.test.ts index fdf9ef0f9f2..e538186e58c 100644 --- a/test/abilities/normal-move-type-change.test.ts +++ b/test/abilities/normal-move-type-change.test.ts @@ -2,9 +2,11 @@ import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; import { allAbilities, allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; +import { attackTypeToHeldItem } from "#items/attack-type-booster"; import { GameManager } from "#test/test-utils/game-manager"; import { toDmgValue } from "#utils/common"; import Phaser from "phaser"; @@ -172,7 +174,7 @@ describe.each([ }); it("should not be affected by silk scarf after changing the move's type", async () => { - game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }]); + game.override.startingHeldItems([{ entry: HeldItemId.SILK_SCARF }]); await game.classicMode.startBattle(); const testMoveInstance = allMoves[MoveId.TACKLE]; @@ -191,7 +193,7 @@ describe.each([ }); it("should be affected by the type boosting item after changing the move's type", async () => { - game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: ty }]); + game.override.startingHeldItems([{ entry: attackTypeToHeldItem[ty] }]); await game.classicMode.startBattle(); // get the power boost from the ability so we can compare it to the item diff --git a/test/abilities/normalize.test.ts b/test/abilities/normalize.test.ts index a19a08fdaf0..610e125387d 100644 --- a/test/abilities/normalize.test.ts +++ b/test/abilities/normalize.test.ts @@ -1,6 +1,7 @@ import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; @@ -57,9 +58,7 @@ describe("Abilities - Normalize", () => { }); it("should not apply the old type boost item after changing a move's type", async () => { - game.override - .startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.GRASS }]) - .moveset([MoveId.LEAFAGE]); + game.override.startingHeldItems([{ entry: HeldItemId.MIRACLE_SEED }]).moveset([MoveId.LEAFAGE]); const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower"); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -71,9 +70,7 @@ describe("Abilities - Normalize", () => { }); it("should apply silk scarf's power boost after changing a move's type", async () => { - game.override - .startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }]) - .moveset([MoveId.LEAFAGE]); + game.override.startingHeldItems([{ entry: HeldItemId.SILK_SCARF }]).moveset([MoveId.LEAFAGE]); const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower"); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); diff --git a/test/abilities/protosynthesis.test.ts b/test/abilities/protosynthesis.test.ts index f4461a562ea..1bb1dcf124f 100644 --- a/test/abilities/protosynthesis.test.ts +++ b/test/abilities/protosynthesis.test.ts @@ -4,6 +4,7 @@ import { MoveId } from "#enums/move-id"; import { Nature } from "#enums/nature"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -37,7 +38,7 @@ describe("Abilities - Protosynthesis", () => { it("should not consider temporary items when determining which stat to boost", async () => { // Mew has uniform base stats game.override - .startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.DEF }]) + .startingTrainerItems([{ entry: TrainerItemId.X_DEFENSE }]) .enemyMoveset(MoveId.SUNNY_DAY) .startingLevel(100) .enemyLevel(100); diff --git a/test/abilities/unburden.test.ts b/test/abilities/unburden.test.ts index 686a6c20904..745b8426055 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -2,12 +2,12 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; +import { TrainerItemId } from "#enums/trainer-item-id"; import type { Pokemon } from "#field/pokemon"; -import type { ContactHeldItemTransferChanceModifier } from "#modifiers/modifier"; import { StealHeldItemChanceAttr } from "#moves/move"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; @@ -21,7 +21,7 @@ describe("Abilities - Unburden", () => { * Count the number of held items a Pokemon has, accounting for stacks of multiple items. */ function getHeldItemCount(pokemon: Pokemon): number { - const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount()); + const stackCounts = pokemon.getHeldItems().map(t => pokemon.heldItemManager.getStack(t)); return stackCounts.reduce((a, b) => a + b, 0); } @@ -43,19 +43,16 @@ describe("Abilities - Unburden", () => { .ability(AbilityId.UNBURDEN) .moveset([MoveId.SPLASH, MoveId.KNOCK_OFF, MoveId.PLUCK, MoveId.FALSE_SWIPE]) .startingHeldItems([ - { name: "BERRY", count: 1, type: BerryType.SITRUS }, - { name: "BERRY", count: 2, type: BerryType.APICOT }, - { name: "BERRY", count: 2, type: BerryType.LUM }, + { entry: HeldItemId.SITRUS_BERRY }, + { entry: HeldItemId.APICOT_BERRY, count: 2 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ]) .enemySpecies(SpeciesId.NINJASK) .enemyLevel(100) .enemyMoveset(MoveId.SPLASH) .enemyAbility(AbilityId.UNBURDEN) .enemyPassiveAbility(AbilityId.NO_GUARD) - .enemyHeldItems([ - { name: "BERRY", type: BerryType.SITRUS, count: 1 }, - { name: "BERRY", type: BerryType.LUM, count: 1 }, - ]); + .enemyHeldItems([{ entry: HeldItemId.SITRUS_BERRY }, { entry: HeldItemId.LUM_BERRY }]); // For the various tests that use Thief, give it a 100% steal rate vi.spyOn(allMoves[MoveId.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); }); @@ -77,7 +74,9 @@ describe("Abilities - Unburden", () => { }); it("should activate when a berry is eaten, even if Berry Pouch preserves the berry", async () => { - game.override.enemyMoveset(MoveId.FALSE_SWIPE).startingModifier([{ name: "BERRY_POUCH", count: 5850 }]); + game.override + .enemyMoveset(MoveId.FALSE_SWIPE) + .startingTrainerItems([{ entry: TrainerItemId.BERRY_POUCH, count: 5850 }]); await game.classicMode.startBattle([SpeciesId.TREECKO]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -174,13 +173,9 @@ describe("Abilities - Unburden", () => { }); it("should activate when an item is stolen via grip claw", async () => { - game.override.startingHeldItems([{ name: "GRIP_CLAW", count: 1 }]); + game.override.startingHeldItems([{ entry: HeldItemId.GRIP_CLAW, count: 10 }]); await game.classicMode.startBattle([SpeciesId.TREECKO]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; - vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); - const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyHeldItemCt = getHeldItemCount(enemyPokemon); const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); @@ -267,7 +262,7 @@ describe("Abilities - Unburden", () => { }); it("should not activate when passing a baton to a teammate switching in", async () => { - game.override.startingHeldItems([{ name: "BATON" }]).moveset(MoveId.BATON_PASS); + game.override.startingHeldItems([{ entry: HeldItemId.BATON }]).moveset(MoveId.BATON_PASS); await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.PURRLOIN]); const [treecko, purrloin] = game.scene.getPlayerParty(); @@ -314,7 +309,7 @@ describe("Abilities - Unburden", () => { }); it("should activate when a reviver seed is used", async () => { - game.override.startingHeldItems([{ name: "REVIVER_SEED" }]).enemyMoveset([MoveId.WING_ATTACK]); + game.override.startingHeldItems([{ entry: HeldItemId.REVIVER_SEED }]).enemyMoveset([MoveId.WING_ATTACK]); await game.classicMode.startBattle([SpeciesId.TREECKO]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -359,7 +354,7 @@ describe("Abilities - Unburden", () => { .battleStyle("double") .enemyMoveset([MoveId.SPLASH, MoveId.THIEF]) .moveset([MoveId.SPLASH, MoveId.REVIVAL_BLESSING]) - .startingHeldItems([{ name: "WIDE_LENS" }]); + .startingHeldItems([{ entry: HeldItemId.WIDE_LENS }]); await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.FEEBAS, SpeciesId.MILOTIC]); const treecko = game.scene.getPlayerField()[0]; diff --git a/test/abilities/wimp-out.test.ts b/test/abilities/wimp-out.test.ts index a1c19a12fd4..3974ca19c4b 100644 --- a/test/abilities/wimp-out.test.ts +++ b/test/abilities/wimp-out.test.ts @@ -4,6 +4,7 @@ import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -205,7 +206,7 @@ describe("Abilities - Wimp Out", () => { game.override .moveset([MoveId.DOUBLE_EDGE]) .enemyMoveset([MoveId.SPLASH]) - .startingHeldItems([{ name: "SHELL_BELL", count: 4 }]); + .startingHeldItems([{ entry: HeldItemId.SHELL_BELL, count: 4 }]); await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]); const wimpod = game.scene.getPlayerPokemon()!; @@ -430,7 +431,7 @@ describe("Abilities - Wimp Out", () => { }); it("triggers after last hit of multi hit move (multi lens)", async () => { - game.override.enemyMoveset(MoveId.TACKLE).enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]); + game.override.enemyMoveset(MoveId.TACKLE).enemyHeldItems([{ entry: HeldItemId.MULTI_LENS, count: 1 }]); await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]); game.scene.getPlayerPokemon()!.hp *= 0.51; @@ -547,7 +548,7 @@ describe("Abilities - Wimp Out", () => { await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.selectEnemyMove(MoveId.ENDURE); - await game.phaseInterceptor.to("SelectModifierPhase"); + await game.phaseInterceptor.to("SelectRewardPhase"); expect(game.scene.currentBattle.waveIndex).toBe(wave + 1); }); }); diff --git a/test/achievements/achievement.test.ts b/test/achievements/achievement.test.ts index 9060d6213cc..7afadc5d2ad 100644 --- a/test/achievements/achievement.test.ts +++ b/test/achievements/achievement.test.ts @@ -1,13 +1,16 @@ import type { BattleScene } from "#app/battle-scene"; -import { TurnHeldItemTransferModifier } from "#modifiers/modifier"; +import { allSpecies } from "#data/data-lists"; +import { HeldItemId } from "#enums/held-item-id"; +import { SpeciesId } from "#enums/species-id"; +import { PlayerPokemon } from "#field/pokemon"; import { Achv, AchvTier, achvs, DamageAchv, HealAchv, + HeldItemAchv, LevelAchv, - ModifierAchv, MoneyAchv, RibbonAchv, } from "#system/achv"; @@ -204,32 +207,26 @@ describe("LevelAchv", () => { }); }); -describe("ModifierAchv", () => { +describe("HeldItemAchv", () => { it("should create an instance of ModifierAchv", () => { - const modifierAchv = new ModifierAchv( + const heldItemAchv = new HeldItemAchv( "", - "Test Modifier Achievement", + "Test Held Item Achievement", "Test Description", "modifier_icon", 10, () => true, ); - expect(modifierAchv).toBeInstanceOf(ModifierAchv); - expect(modifierAchv instanceof Achv).toBe(true); + expect(heldItemAchv).toBeInstanceOf(HeldItemAchv); + expect(heldItemAchv instanceof Achv).toBe(true); }); - it("should validate the achievement based on the modifier function", () => { - const modifierAchv = new ModifierAchv( - "", - "Test Modifier Achievement", - "Test Description", - "modifier_icon", - 10, - () => true, - ); - const modifier = new TurnHeldItemTransferModifier(null!, 3, 1); - - expect(modifierAchv.validate([modifier])).toBe(true); + it("should validate the mini black hole achievement", () => { + const heldItemAchv = achvs.MINI_BLACK_HOLE; + const pokemon = new PlayerPokemon(allSpecies[SpeciesId.BULBASAUR], 1); + expect(heldItemAchv.validate([pokemon])).toBe(false); + pokemon.heldItemManager.add(HeldItemId.MINI_BLACK_HOLE); + expect(heldItemAchv.validate([pokemon])).toBe(true); }); }); @@ -262,7 +259,7 @@ describe("achvs", () => { expect(achvs.TERASTALLIZE).toBeInstanceOf(Achv); expect(achvs.STELLAR_TERASTALLIZE).toBeInstanceOf(Achv); expect(achvs.SPLICE).toBeInstanceOf(Achv); - expect(achvs.MINI_BLACK_HOLE).toBeInstanceOf(ModifierAchv); + expect(achvs.MINI_BLACK_HOLE).toBeInstanceOf(HeldItemAchv); expect(achvs.CATCH_MYTHICAL).toBeInstanceOf(Achv); expect(achvs.CATCH_SUB_LEGENDARY).toBeInstanceOf(Achv); expect(achvs.CATCH_LEGENDARY).toBeInstanceOf(Achv); diff --git a/test/battle/battle.test.ts b/test/battle/battle.test.ts index ff5090e5f8d..c37a24ca564 100644 --- a/test/battle/battle.test.ts +++ b/test/battle/battle.test.ts @@ -6,7 +6,7 @@ import { GameModes } from "#enums/game-modes"; import { MoveId } from "#enums/move-id"; import { PlayerGender } from "#enums/player-gender"; import { SpeciesId } from "#enums/species-id"; -import { Stat } from "#enums/stat"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; import { BattleEndPhase } from "#phases/battle-end-phase"; import { CommandPhase } from "#phases/command-phase"; @@ -93,7 +93,7 @@ describe("Phase - Battle Phase", () => { game.override.enemySpecies(SpeciesId.RATTATA).startingLevel(2000).battleStyle("single").startingWave(3); await game.classicMode.startBattle([SpeciesId.MEWTWO]); game.move.use(MoveId.TACKLE); - await game.phaseInterceptor.to("SelectModifierPhase"); + await game.phaseInterceptor.to("SelectRewardPhase"); }); it("do attack wave 3 - single battle - regular - NO OHKO with opponent using non damage attack", async () => { @@ -290,7 +290,7 @@ describe("Phase - Battle Phase", () => { .startingLevel(100) .moveset([moveToUse]) .enemyMoveset(MoveId.SPLASH) - .startingHeldItems([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }]); + .startingTrainerItems([{ entry: TrainerItemId.X_ACCURACY }]); await game.classicMode.startBattle(); game.scene.getPlayerPokemon()!.hp = 1; diff --git a/test/battle/damage-calculation.test.ts b/test/battle/damage-calculation.test.ts index ca01830abd0..bc05286b845 100644 --- a/test/battle/damage-calculation.test.ts +++ b/test/battle/damage-calculation.test.ts @@ -1,9 +1,9 @@ -import { allMoves, modifierTypes } from "#data/data-lists"; +import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import type { EnemyPersistentModifier } from "#modifiers/modifier"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -71,9 +71,7 @@ describe("Battle Mechanics - Damage Calculation", () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const dmg_redux_modifier = modifierTypes.ENEMY_DAMAGE_REDUCTION().newModifier() as EnemyPersistentModifier; - dmg_redux_modifier.stackCount = 1000; - await game.scene.addEnemyModifier(modifierTypes.ENEMY_DAMAGE_REDUCTION().newModifier() as EnemyPersistentModifier); + game.scene.enemyTrainerItems.add(TrainerItemId.ENEMY_DAMAGE_REDUCTION, 1000); const aggron = game.scene.getEnemyPokemon()!; diff --git a/test/daily-mode.test.ts b/test/daily-mode.test.ts index 34a8da80478..425ebae2879 100644 --- a/test/daily-mode.test.ts +++ b/test/daily-mode.test.ts @@ -2,10 +2,10 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { BiomeId } from "#enums/biome-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; -import { MapModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Daily Mode", () => { @@ -36,7 +36,7 @@ describe("Daily Mode", () => { expect(pkm.level).toBe(20); expect(pkm.moveset.length).toBeGreaterThan(0); }); - expect(game.scene.getModifiers(MapModifier).length).toBeGreaterThan(0); + expect(game.scene.trainerItems.getStack(TrainerItemId.MAP)).toBe(1); }); }); @@ -74,8 +74,8 @@ describe("Shop modifications", async () => { game.move.select(MoveId.SPLASH); await game.doKillOpponents(); await game.phaseInterceptor.to("BattleEndPhase"); - game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, () => { - expect(game.scene.ui.getHandler()).toBeInstanceOf(ModifierSelectUiHandler); + game.onNextPrompt("SelectRewardPhase", UiMode.REWARD_SELECT, () => { + expect(game.scene.ui.getHandler()).toBeInstanceOf(RewardSelectUiHandler); game.modifiers.testCheck("EVIOLITE", false).testCheck("MINI_BLACK_HOLE", false); }); }); @@ -85,8 +85,8 @@ describe("Shop modifications", async () => { game.move.select(MoveId.SPLASH); await game.doKillOpponents(); await game.phaseInterceptor.to("BattleEndPhase"); - game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, () => { - expect(game.scene.ui.getHandler()).toBeInstanceOf(ModifierSelectUiHandler); + game.onNextPrompt("SelectRewardPhase", UiMode.REWARD_SELECT, () => { + expect(game.scene.ui.getHandler()).toBeInstanceOf(RewardSelectUiHandler); game.modifiers.testCheck("EVIOLITE", true).testCheck("MINI_BLACK_HOLE", true); }); }); diff --git a/test/final-boss.test.ts b/test/final-boss.test.ts index 2180979899a..c9d64ba487a 100644 --- a/test/final-boss.test.ts +++ b/test/final-boss.test.ts @@ -1,10 +1,10 @@ import { AbilityId } from "#enums/ability-id"; import { BiomeId } from "#enums/biome-id"; import { GameModes } from "#enums/game-modes"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; -import { TurnHeldItemTransferModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -90,9 +90,7 @@ describe("Final Boss", () => { expect(eternatus.formIndex).toBe(1); expect(eternatus.bossSegments).toBe(5); expect(eternatus.bossSegmentIndex).toBe(4); - const miniBlackHole = eternatus.getHeldItems().find(m => m instanceof TurnHeldItemTransferModifier); - expect(miniBlackHole).toBeDefined(); - expect(miniBlackHole?.stackCount).toBe(1); + expect(eternatus.heldItemManager.getStack(HeldItemId.MINI_BLACK_HOLE)).toBe(1); }); it("should change form on status damage down to last boss fragment", async () => { @@ -127,8 +125,6 @@ describe("Final Boss", () => { expect(eternatus.formIndex).toBe(1); expect(eternatus.bossSegments).toBe(5); expect(eternatus.bossSegmentIndex).toBe(4); - const miniBlackHole = eternatus.getHeldItems().find(m => m instanceof TurnHeldItemTransferModifier); - expect(miniBlackHole).toBeDefined(); - expect(miniBlackHole?.stackCount).toBe(1); + expect(eternatus.heldItemManager.getStack(HeldItemId.MINI_BLACK_HOLE)).toBe(1); }); }); diff --git a/test/items/dire-hit.test.ts b/test/items/dire-hit.test.ts index fe7fabd3c4c..de20e378b4e 100644 --- a/test/items/dire-hit.test.ts +++ b/test/items/dire-hit.test.ts @@ -2,15 +2,15 @@ import { Button } from "#enums/buttons"; import { MoveId } from "#enums/move-id"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; -import { TempCritBoosterModifier } from "#modifiers/modifier"; import { BattleEndPhase } from "#phases/battle-end-phase"; import { CommandPhase } from "#phases/command-phase"; import { NewBattlePhase } from "#phases/new-battle-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { GameManager } from "#test/test-utils/game-manager"; -import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import type { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -35,7 +35,7 @@ describe("Items - Dire Hit", () => { .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.SPLASH) .moveset([MoveId.POUND]) - .startingHeldItems([{ name: "DIRE_HIT" }]) + .startingTrainerItems([{ entry: TrainerItemId.DIRE_HIT }]) .battleStyle("single"); }); @@ -63,15 +63,15 @@ describe("Items - Dire Hit", () => { await game.phaseInterceptor.to(BattleEndPhase); - const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier; - expect(modifier.getBattleCount()).toBe(4); + const stack = game.scene.trainerItems.getStack(TrainerItemId.DIRE_HIT); + expect(stack).toBe(4); // Forced DIRE_HIT to spawn in the first slot with override game.onNextPrompt( - "SelectModifierPhase", - UiMode.MODIFIER_SELECT, + "SelectRewardPhase", + UiMode.REWARD_SELECT, () => { - const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler; + const handler = game.scene.ui.getHandler() as RewardSelectUiHandler; // Traverse to first modifier slot handler.setCursor(0); handler.setRowCursor(ShopCursorTarget.REWARDS); @@ -83,14 +83,7 @@ describe("Items - Dire Hit", () => { await game.phaseInterceptor.to(TurnInitPhase); - // Making sure only one booster is in the modifier list even after picking up another - let count = 0; - for (const m of game.scene.modifiers) { - if (m instanceof TempCritBoosterModifier) { - count++; - expect((m as TempCritBoosterModifier).getBattleCount()).toBe(5); - } - } - expect(count).toBe(1); + const newStack = game.scene.trainerItems.getStack(TrainerItemId.DIRE_HIT); + expect(newStack).toBe(5); }); }); diff --git a/test/items/double-battle-chance-booster.test.ts b/test/items/double-battle-chance-booster.test.ts index 2c12b34eba3..9483095104d 100644 --- a/test/items/double-battle-chance-booster.test.ts +++ b/test/items/double-battle-chance-booster.test.ts @@ -2,10 +2,10 @@ import { Button } from "#enums/buttons"; import { MoveId } from "#enums/move-id"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; -import { DoubleBattleChanceBoosterModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; -import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import type { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -27,7 +27,9 @@ describe("Items - Double Battle Chance Boosters", () => { }); it("should guarantee double battle with 2 unique tiers", async () => { - game.override.startingModifier([{ name: "LURE" }, { name: "SUPER_LURE" }]).startingWave(2); + game.override + .startingTrainerItems([{ entry: TrainerItemId.LURE }, { entry: TrainerItemId.SUPER_LURE }]) + .startingWave(2); await game.classicMode.startBattle(); @@ -35,7 +37,13 @@ describe("Items - Double Battle Chance Boosters", () => { }); it("should guarantee double boss battle with 3 unique tiers", async () => { - game.override.startingModifier([{ name: "LURE" }, { name: "SUPER_LURE" }, { name: "MAX_LURE" }]).startingWave(10); + game.override + .startingTrainerItems([ + { entry: TrainerItemId.LURE }, + { entry: TrainerItemId.SUPER_LURE }, + { entry: TrainerItemId.MAX_LURE }, + ]) + .startingWave(10); await game.classicMode.startBattle(); @@ -48,7 +56,7 @@ describe("Items - Double Battle Chance Boosters", () => { it("should renew how many battles are left of existing booster when picking up new booster of same tier", async () => { game.override - .startingModifier([{ name: "LURE" }]) + .startingTrainerItems([{ entry: TrainerItemId.LURE }]) .itemRewards([{ name: "LURE" }]) .moveset(MoveId.SPLASH) .startingLevel(200); @@ -61,17 +69,15 @@ describe("Items - Double Battle Chance Boosters", () => { await game.phaseInterceptor.to("BattleEndPhase"); - const modifier = game.scene.findModifier( - m => m instanceof DoubleBattleChanceBoosterModifier, - ) as DoubleBattleChanceBoosterModifier; - expect(modifier.getBattleCount()).toBe(9); + const stack = game.scene.trainerItems.getStack(TrainerItemId.LURE); + expect(stack).toBe(9); // Forced LURE to spawn in the first slot with override game.onNextPrompt( - "SelectModifierPhase", - UiMode.MODIFIER_SELECT, + "SelectRewardPhase", + UiMode.REWARD_SELECT, () => { - const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler; + const handler = game.scene.ui.getHandler() as RewardSelectUiHandler; // Traverse to first modifier slot handler.setCursor(0); handler.setRowCursor(ShopCursorTarget.REWARDS); @@ -84,14 +90,7 @@ describe("Items - Double Battle Chance Boosters", () => { await game.phaseInterceptor.to("TurnInitPhase"); // Making sure only one booster is in the modifier list even after picking up another - let count = 0; - for (const m of game.scene.modifiers) { - if (m instanceof DoubleBattleChanceBoosterModifier) { - count++; - const modifierInstance = m as DoubleBattleChanceBoosterModifier; - expect(modifierInstance.getBattleCount()).toBe(modifierInstance.getMaxBattles()); - } - } - expect(count).toBe(1); + const newStack = game.scene.trainerItems.getStack(TrainerItemId.LURE); + expect(newStack).toBe(10); }); }); diff --git a/test/items/eviolite.test.ts b/test/items/eviolite.test.ts index 2e64135d264..19c50ba90b4 100644 --- a/test/items/eviolite.test.ts +++ b/test/items/eviolite.test.ts @@ -1,6 +1,8 @@ +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { StatBoosterModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import { GameManager } from "#test/test-utils/game-manager"; import { NumberHolder, randItem } from "#utils/common"; import Phaser from "phaser"; @@ -22,7 +24,7 @@ describe("Items - Eviolite", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single").startingHeldItems([{ name: "EVIOLITE" }]); + game.override.battleStyle("single").startingHeldItems([{ entry: HeldItemId.EVIOLITE }]); }); it("should provide 50% boost to DEF and SPDEF for unevolved, unfused pokemon", async () => { @@ -32,9 +34,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); - - // Ignore other calculations for simplicity + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); return Math.floor(statValue.value); }); @@ -53,7 +53,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -83,7 +83,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -113,7 +113,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -143,7 +143,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -173,7 +173,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity @@ -203,7 +203,7 @@ describe("Items - Eviolite", () => { vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { const statValue = new NumberHolder(partyMember.getStat(stat, false)); - game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: stat, statValue: statValue }); // Ignore other calculations for simplicity diff --git a/test/items/exp-booster.test.ts b/test/items/exp-booster.test.ts index dd2c8eb0c2b..87d22350c00 100644 --- a/test/items/exp-booster.test.ts +++ b/test/items/exp-booster.test.ts @@ -1,5 +1,7 @@ import { AbilityId } from "#enums/ability-id"; -import { PokemonExpBoosterModifier } from "#modifiers/modifier"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; +import { applyHeldItems } from "#items/all-held-items"; import { GameManager } from "#test/test-utils/game-manager"; import { NumberHolder } from "#utils/common"; import Phaser from "phaser"; @@ -26,13 +28,13 @@ describe("EXP Modifier Items", () => { }); it("EXP booster items stack multiplicatively", async () => { - game.override.startingHeldItems([{ name: "LUCKY_EGG", count: 3 }, { name: "GOLDEN_EGG" }]); + game.override.startingHeldItems([{ entry: HeldItemId.LUCKY_EGG, count: 3 }, { entry: HeldItemId.GOLDEN_EGG }]); await game.classicMode.startBattle(); const partyMember = game.scene.getPlayerPokemon()!; partyMember.exp = 100; const expHolder = new NumberHolder(partyMember.exp); - game.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, expHolder); + applyHeldItems(HeldItemEffect.EXP_BOOSTER, { pokemon: partyMember, expAmount: expHolder }); expect(expHolder.value).toBe(440); }); }); diff --git a/test/items/grip-claw.test.ts b/test/items/grip-claw.test.ts index 5ffebd76946..2bb65146316 100644 --- a/test/items/grip-claw.test.ts +++ b/test/items/grip-claw.test.ts @@ -1,13 +1,11 @@ import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; -import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import type { Pokemon } from "#field/pokemon"; -import type { ContactHeldItemTransferChanceModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Items - Grip Claw", () => { let phaserGame: Phaser.Game; @@ -29,14 +27,14 @@ describe("Items - Grip Claw", () => { game.override .battleStyle("double") .moveset([MoveId.TACKLE, MoveId.SPLASH, MoveId.ATTRACT]) - .startingHeldItems([{ name: "GRIP_CLAW", count: 1 }]) + .startingHeldItems([{ entry: HeldItemId.GRIP_CLAW }]) .enemySpecies(SpeciesId.SNORLAX) .enemyAbility(AbilityId.UNNERVE) .ability(AbilityId.UNNERVE) .enemyMoveset(MoveId.SPLASH) .enemyHeldItems([ - { name: "BERRY", type: BerryType.SITRUS, count: 2 }, - { name: "BERRY", type: BerryType.LUM, count: 2 }, + { entry: HeldItemId.SITRUS_BERRY, count: 2 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ]) .enemyLevel(100); }); @@ -45,15 +43,12 @@ describe("Items - Grip Claw", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); const [playerPokemon] = game.scene.getPlayerField(); - - const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; - vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); - const enemyPokemon = game.scene.getEnemyField(); + playerPokemon.heldItemManager.setStack(HeldItemId.GRIP_CLAW, 10); - const playerHeldItemCount = getHeldItemCount(playerPokemon); - const enemy1HeldItemCount = getHeldItemCount(enemyPokemon[0]); - const enemy2HeldItemCount = getHeldItemCount(enemyPokemon[1]); + const playerHeldItemCount = playerPokemon.heldItemManager.getHeldItemCount(); + const enemy1HeldItemCount = enemyPokemon[0].heldItemManager.getHeldItemCount(); + const enemy2HeldItemCount = enemyPokemon[1].heldItemManager.getHeldItemCount(); expect(enemy2HeldItemCount).toBeGreaterThan(0); game.move.select(MoveId.TACKLE, 0, BattlerIndex.ENEMY_2); @@ -61,9 +56,9 @@ describe("Items - Grip Claw", () => { await game.phaseInterceptor.to("BerryPhase", false); - const playerHeldItemCountAfter = getHeldItemCount(playerPokemon); - const enemy1HeldItemCountsAfter = getHeldItemCount(enemyPokemon[0]); - const enemy2HeldItemCountsAfter = getHeldItemCount(enemyPokemon[1]); + const playerHeldItemCountAfter = playerPokemon.heldItemManager.getHeldItemCount(); + const enemy1HeldItemCountsAfter = enemyPokemon[0].heldItemManager.getHeldItemCount(); + const enemy2HeldItemCountsAfter = enemyPokemon[1].heldItemManager.getHeldItemCount(); expect(playerHeldItemCountAfter).toBe(playerHeldItemCount + 1); expect(enemy1HeldItemCountsAfter).toBe(enemy1HeldItemCount); @@ -74,15 +69,11 @@ describe("Items - Grip Claw", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); const [playerPokemon] = game.scene.getPlayerField(); - - const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; - vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); - const enemyPokemon = game.scene.getEnemyField(); - const playerHeldItemCount = getHeldItemCount(playerPokemon); - const enemy1HeldItemCount = getHeldItemCount(enemyPokemon[0]); - const enemy2HeldItemCount = getHeldItemCount(enemyPokemon[1]); + const playerHeldItemCount = playerPokemon.heldItemManager.getHeldItemCount(); + const enemy1HeldItemCount = enemyPokemon[0].heldItemManager.getHeldItemCount(); + const enemy2HeldItemCount = enemyPokemon[1].heldItemManager.getHeldItemCount(); expect(enemy2HeldItemCount).toBeGreaterThan(0); game.move.select(MoveId.ATTRACT, 0, BattlerIndex.ENEMY_2); @@ -90,9 +81,9 @@ describe("Items - Grip Claw", () => { await game.phaseInterceptor.to("BerryPhase", false); - const playerHeldItemCountAfter = getHeldItemCount(playerPokemon); - const enemy1HeldItemCountsAfter = getHeldItemCount(enemyPokemon[0]); - const enemy2HeldItemCountsAfter = getHeldItemCount(enemyPokemon[1]); + const playerHeldItemCountAfter = playerPokemon.heldItemManager.getHeldItemCount(); + const enemy1HeldItemCountsAfter = enemyPokemon[0].heldItemManager.getHeldItemCount(); + const enemy2HeldItemCountsAfter = enemyPokemon[1].heldItemManager.getHeldItemCount(); expect(playerHeldItemCountAfter).toBe(playerHeldItemCount); expect(enemy1HeldItemCountsAfter).toBe(enemy1HeldItemCount); @@ -103,31 +94,19 @@ describe("Items - Grip Claw", () => { game.override .battleStyle("double") .moveset([MoveId.POLLEN_PUFF, MoveId.ENDURE]) - .startingHeldItems([ - { name: "GRIP_CLAW", count: 1 }, - { name: "BERRY", type: BerryType.LUM, count: 1 }, - ]); + .startingHeldItems([{ entry: HeldItemId.GRIP_CLAW }, { entry: HeldItemId.LUM_BERRY }]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.OMANYTE]); const [leftPokemon, rightPokemon] = game.scene.getPlayerField(); + leftPokemon.heldItemManager.setStack(HeldItemId.GRIP_CLAW, 10); - const gripClaw = leftPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; - vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); - - const heldItemCountBefore = getHeldItemCount(rightPokemon); + const heldItemCountBefore = rightPokemon.heldItemManager.getHeldItemCount(); game.move.select(MoveId.POLLEN_PUFF, 0, BattlerIndex.PLAYER_2); game.move.select(MoveId.ENDURE, 1); await game.toNextTurn(); - expect(getHeldItemCount(rightPokemon)).toBe(heldItemCountBefore); + expect(rightPokemon.heldItemManager.getHeldItemCount()).toBe(heldItemCountBefore); }); }); - -/* - * Gets the total number of items a Pokemon holds - */ -function getHeldItemCount(pokemon: Pokemon) { - return pokemon.getHeldItems().reduce((currentTotal, item) => currentTotal + item.getStackCount(), 0); -} diff --git a/test/items/leek.test.ts b/test/items/leek.test.ts index c38294d07a4..e2eadba7756 100644 --- a/test/items/leek.test.ts +++ b/test/items/leek.test.ts @@ -1,3 +1,4 @@ +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { TurnEndPhase } from "#phases/turn-end-phase"; @@ -26,7 +27,7 @@ describe("Items - Leek", () => { game.override .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.SPLASH) - .startingHeldItems([{ name: "LEEK" }]) + .startingHeldItems([{ entry: HeldItemId.LEEK }]) .moveset([MoveId.TACKLE]) .battleStyle("single"); }); diff --git a/test/items/leftovers.test.ts b/test/items/leftovers.test.ts index bed40b1c83a..e9a4dd115e1 100644 --- a/test/items/leftovers.test.ts +++ b/test/items/leftovers.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { DamageAnimPhase } from "#phases/damage-anim-phase"; @@ -31,17 +32,17 @@ describe("Items - Leftovers", () => { .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.UNNERVE) .enemyMoveset(MoveId.TACKLE) - .startingHeldItems([{ name: "LEFTOVERS", count: 1 }]); + .startingHeldItems([{ entry: HeldItemId.LEFTOVERS }]); }); it("leftovers works", async () => { await game.classicMode.startBattle([SpeciesId.ARCANINE]); - // Make sure leftovers are there - expect(game.scene.modifiers[0].type.id).toBe("LEFTOVERS"); - const leadPokemon = game.scene.getPlayerPokemon()!; + // Make sure leftovers are there + expect(leadPokemon.heldItemManager.hasItem(HeldItemId.LEFTOVERS)).toBe(true); + // We should have full hp expect(leadPokemon.isFullHp()).toBe(true); diff --git a/test/items/light-ball.test.ts b/test/items/light-ball.test.ts index a7f41255ff3..74f88f695c1 100644 --- a/test/items/light-ball.test.ts +++ b/test/items/light-ball.test.ts @@ -1,7 +1,8 @@ -import { modifierTypes } from "#data/data-lists"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import i18next from "#plugins/i18n"; import { GameManager } from "#test/test-utils/game-manager"; import { NumberHolder } from "#utils/common"; @@ -29,7 +30,7 @@ describe("Items - Light Ball", () => { }); it("LIGHT_BALL activates in battle correctly", async () => { - game.override.startingHeldItems([{ name: "RARE_SPECIES_STAT_BOOSTER", type: "LIGHT_BALL" }]); + game.override.startingHeldItems([{ entry: HeldItemId.LIGHT_BALL }]); const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.PIKACHU]); @@ -91,20 +92,17 @@ describe("Items - Light Ball", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: atkValue }); const spAtkValue = new NumberHolder(spAtkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue); - + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPDEF, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["LIGHT_BALL"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); + // Giving Light Ball to party member and testing if it applies + partyMember.heldItemManager.add(HeldItemId.LIGHT_BALL); + + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPATK, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); @@ -130,20 +128,18 @@ describe("Items - Light Ball", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: atkValue }); const spAtkValue = new NumberHolder(spAtkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPDEF, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["LIGHT_BALL"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); + // Giving Light Ball to party member and testing if it applies + partyMember.heldItemManager.add(HeldItemId.LIGHT_BALL); + + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPATK, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); @@ -169,20 +165,18 @@ describe("Items - Light Ball", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: atkValue }); const spAtkValue = new NumberHolder(spAtkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPDEF, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["LIGHT_BALL"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); + // Giving Light Ball to party member and testing if it applies + partyMember.heldItemManager.add(HeldItemId.LIGHT_BALL); + + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPATK, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(2); expect(spAtkValue.value / spAtkStat).toBe(2); @@ -198,20 +192,17 @@ describe("Items - Light Ball", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: atkValue }); const spAtkValue = new NumberHolder(spAtkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPDEF, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); - // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["LIGHT_BALL"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); + // Giving Light Ball to party member and testing if it applies + partyMember.heldItemManager.add(HeldItemId.LIGHT_BALL); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPATK, statValue: spAtkValue }); expect(atkValue.value / atkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1); diff --git a/test/items/lock-capsule.test.ts b/test/items/lock-capsule.test.ts index 01552a4db37..435b6814bc4 100644 --- a/test/items/lock-capsule.test.ts +++ b/test/items/lock-capsule.test.ts @@ -1,8 +1,9 @@ import { AbilityId } from "#enums/ability-id"; -import { ModifierTier } from "#enums/modifier-tier"; import { MoveId } from "#enums/move-id"; +import { RarityTier } from "#enums/reward-tier"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -29,25 +30,25 @@ describe("Items - Lock Capsule", () => { .startingLevel(200) .moveset([MoveId.SURF]) .enemyAbility(AbilityId.BALL_FETCH) - .startingModifier([{ name: "LOCK_CAPSULE" }]); + .startingTrainerItems([{ entry: TrainerItemId.LOCK_CAPSULE }]); }); it("doesn't set the cost of common tier items to 0", async () => { await game.classicMode.startBattle(); game.scene.phaseManager.overridePhase( - new SelectModifierPhase(0, undefined, { - guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.COMMON], + new SelectRewardPhase(0, undefined, { + guaranteedRarityTiers: [RarityTier.COMMON, RarityTier.COMMON, RarityTier.COMMON], fillRemaining: false, }), ); - game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, () => { - const selectModifierPhase = game.scene.phaseManager.getCurrentPhase() as SelectModifierPhase; - const rerollCost = selectModifierPhase.getRerollCost(true); + game.onNextPrompt("SelectRewardPhase", UiMode.REWARD_SELECT, () => { + const selectRewardPhase = game.scene.phaseManager.getCurrentPhase() as SelectRewardPhase; + const rerollCost = selectRewardPhase.getRerollCost(true); expect(rerollCost).toBe(150); }); game.doSelectModifier(); - await game.phaseInterceptor.to("SelectModifierPhase"); + await game.phaseInterceptor.to("SelectRewardPhase"); }); }); diff --git a/test/items/metal-powder.test.ts b/test/items/metal-powder.test.ts index 4dac8dd39b1..9cb069c73ce 100644 --- a/test/items/metal-powder.test.ts +++ b/test/items/metal-powder.test.ts @@ -1,7 +1,8 @@ -import { modifierTypes } from "#data/data-lists"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import i18next from "#plugins/i18n"; import { GameManager } from "#test/test-utils/game-manager"; import { NumberHolder } from "#utils/common"; @@ -29,7 +30,7 @@ describe("Items - Metal Powder", () => { }); it("METAL_POWDER activates in battle correctly", async () => { - game.override.startingHeldItems([{ name: "RARE_SPECIES_STAT_BOOSTER", type: "METAL_POWDER" }]); + game.override.startingHeldItems([{ entry: HeldItemId.METAL_POWDER }]); const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.DITTO]); @@ -90,16 +91,13 @@ describe("Items - Metal Powder", () => { // Making sure modifier is not applied without holding item const defValue = new NumberHolder(defStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: defValue }); expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["METAL_POWDER"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); + partyMember.heldItemManager.add(HeldItemId.METAL_POWDER); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: defValue }); expect(defValue.value / defStat).toBe(2); }); @@ -123,16 +121,13 @@ describe("Items - Metal Powder", () => { // Making sure modifier is not applied without holding item const defValue = new NumberHolder(defStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: defValue }); expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["METAL_POWDER"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); + partyMember.heldItemManager.add(HeldItemId.METAL_POWDER); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: defValue }); expect(defValue.value / defStat).toBe(2); }); @@ -156,16 +151,13 @@ describe("Items - Metal Powder", () => { // Making sure modifier is not applied without holding item const defValue = new NumberHolder(defStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: defValue }); expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["METAL_POWDER"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); + partyMember.heldItemManager.add(HeldItemId.METAL_POWDER); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: defValue }); expect(defValue.value / defStat).toBe(2); }); @@ -179,16 +171,13 @@ describe("Items - Metal Powder", () => { // Making sure modifier is not applied without holding item const defValue = new NumberHolder(defStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: defValue }); expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["METAL_POWDER"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); + partyMember.heldItemManager.add(HeldItemId.METAL_POWDER); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.DEF, statValue: defValue }); expect(defValue.value / defStat).toBe(1); }); diff --git a/test/items/multi-lens.test.ts b/test/items/multi-lens.test.ts index e3cf39e4933..807cd1f41df 100644 --- a/test/items/multi-lens.test.ts +++ b/test/items/multi-lens.test.ts @@ -1,5 +1,6 @@ import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -26,7 +27,7 @@ describe("Items - Multi Lens", () => { game.override .moveset([MoveId.TACKLE, MoveId.TRAILBLAZE, MoveId.TACHYON_CUTTER, MoveId.FUTURE_SIGHT]) .ability(AbilityId.BALL_FETCH) - .startingHeldItems([{ name: "MULTI_LENS" }]) + .startingHeldItems([{ entry: HeldItemId.MULTI_LENS }]) .battleStyle("single") .criticalHits(false) .enemySpecies(SpeciesId.SNORLAX) @@ -42,7 +43,7 @@ describe("Items - Multi Lens", () => { ])( "$stackCount count: should deal {$firstHitDamage}x damage on the first hit, then hit $stackCount times for 0.25x", async ({ stackCount, firstHitDamage }) => { - game.override.startingHeldItems([{ name: "MULTI_LENS", count: stackCount }]); + game.override.startingHeldItems([{ entry: HeldItemId.MULTI_LENS, count: stackCount }]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -116,7 +117,7 @@ describe("Items - Multi Lens", () => { }); it("should enhance fixed-damage moves while also applying damage reduction", async () => { - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]).moveset(MoveId.SEISMIC_TOSS); + game.override.startingHeldItems([{ entry: HeldItemId.MULTI_LENS }]).moveset(MoveId.SEISMIC_TOSS); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -137,7 +138,7 @@ describe("Items - Multi Lens", () => { it("should result in correct damage for hp% attacks with 1 lens", async () => { game.override - .startingHeldItems([{ name: "MULTI_LENS", count: 1 }]) + .startingHeldItems([{ entry: HeldItemId.MULTI_LENS }]) .moveset(MoveId.SUPER_FANG) .ability(AbilityId.COMPOUND_EYES) .enemyLevel(1000) @@ -155,7 +156,7 @@ describe("Items - Multi Lens", () => { it("should result in correct damage for hp% attacks with 2 lenses", async () => { game.override - .startingHeldItems([{ name: "MULTI_LENS", count: 2 }]) + .startingHeldItems([{ entry: HeldItemId.MULTI_LENS, count: 2 }]) .moveset(MoveId.SUPER_FANG) .ability(AbilityId.COMPOUND_EYES) .enemyMoveset(MoveId.SPLASH) @@ -174,7 +175,7 @@ describe("Items - Multi Lens", () => { it("should result in correct damage for hp% attacks with 2 lenses + Parental Bond", async () => { game.override - .startingHeldItems([{ name: "MULTI_LENS", count: 2 }]) + .startingHeldItems([{ entry: HeldItemId.MULTI_LENS, count: 2 }]) .moveset(MoveId.SUPER_FANG) .ability(AbilityId.PARENTAL_BOND) .passiveAbility(AbilityId.COMPOUND_EYES) diff --git a/test/items/mystical-rock.test.ts b/test/items/mystical-rock.test.ts index 3a29c359582..9151f0cb2a4 100644 --- a/test/items/mystical-rock.test.ts +++ b/test/items/mystical-rock.test.ts @@ -1,5 +1,6 @@ import { globalScene } from "#app/global-scene"; import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { GameManager } from "#test/test-utils/game-manager"; @@ -28,7 +29,7 @@ describe("Items - Mystical Rock", () => { .enemyMoveset(MoveId.SPLASH) .enemyAbility(AbilityId.BALL_FETCH) .moveset([MoveId.SUNNY_DAY, MoveId.GRASSY_TERRAIN]) - .startingHeldItems([{ name: "MYSTICAL_ROCK", count: 2 }]) + .startingHeldItems([{ entry: HeldItemId.MYSTICAL_ROCK, count: 2 }]) .battleStyle("single"); }); diff --git a/test/items/quick-powder.test.ts b/test/items/quick-powder.test.ts index 2200e8cf96e..fdab41514b6 100644 --- a/test/items/quick-powder.test.ts +++ b/test/items/quick-powder.test.ts @@ -1,7 +1,8 @@ -import { modifierTypes } from "#data/data-lists"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import i18next from "#plugins/i18n"; import { GameManager } from "#test/test-utils/game-manager"; import { NumberHolder } from "#utils/common"; @@ -29,7 +30,7 @@ describe("Items - Quick Powder", () => { }); it("QUICK_POWDER activates in battle correctly", async () => { - game.override.startingHeldItems([{ name: "RARE_SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]); + game.override.startingHeldItems([{ entry: HeldItemId.QUICK_POWDER }]); const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.DITTO]); @@ -90,16 +91,13 @@ describe("Items - Quick Powder", () => { // Making sure modifier is not applied without holding item const spdValue = new NumberHolder(spdStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPD, statValue: spdValue }); expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["QUICK_POWDER"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); + partyMember.heldItemManager.add(HeldItemId.QUICK_POWDER); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPD, statValue: spdValue }); expect(spdValue.value / spdStat).toBe(2); }); @@ -123,16 +121,13 @@ describe("Items - Quick Powder", () => { // Making sure modifier is not applied without holding item const spdValue = new NumberHolder(spdStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPD, statValue: spdValue }); expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["QUICK_POWDER"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); + partyMember.heldItemManager.add(HeldItemId.QUICK_POWDER); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPD, statValue: spdValue }); expect(spdValue.value / spdStat).toBe(2); }); @@ -156,16 +151,13 @@ describe("Items - Quick Powder", () => { // Making sure modifier is not applied without holding item const spdValue = new NumberHolder(spdStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPD, statValue: spdValue }); expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["QUICK_POWDER"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); + partyMember.heldItemManager.add(HeldItemId.QUICK_POWDER); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPD, statValue: spdValue }); expect(spdValue.value / spdStat).toBe(2); }); @@ -179,16 +171,13 @@ describe("Items - Quick Powder", () => { // Making sure modifier is not applied without holding item const spdValue = new NumberHolder(spdStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPD, statValue: spdValue }); expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["QUICK_POWDER"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); + partyMember.heldItemManager.add(HeldItemId.QUICK_POWDER); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.SPD, statValue: spdValue }); expect(spdValue.value / spdStat).toBe(1); }); diff --git a/test/items/reviver-seed.test.ts b/test/items/reviver-seed.test.ts index 268c5497899..fe6f6a2f232 100644 --- a/test/items/reviver-seed.test.ts +++ b/test/items/reviver-seed.test.ts @@ -1,10 +1,11 @@ -import { allMoves } from "#data/data-lists"; +import { allHeldItems, allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import type { PokemonInstantReviveModifier } from "#modifiers/modifier"; +import type { InstantReviveHeldItem } from "#items/instant-revive"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -32,8 +33,8 @@ describe("Items - Reviver Seed", () => { .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) - .startingHeldItems([{ name: "REVIVER_SEED" }]) - .enemyHeldItems([{ name: "REVIVER_SEED" }]) + .startingHeldItems([{ entry: HeldItemId.REVIVER_SEED }]) + .enemyHeldItems([{ entry: HeldItemId.REVIVER_SEED }]) .enemyMoveset(MoveId.SPLASH); vi.spyOn(allMoves[MoveId.SHEER_COLD], "accuracy", "get").mockReturnValue(100); vi.spyOn(allMoves[MoveId.LEECH_SEED], "accuracy", "get").mockReturnValue(100); @@ -54,7 +55,7 @@ describe("Items - Reviver Seed", () => { const player = game.scene.getPlayerPokemon()!; player.damageAndUpdate(player.hp - 1); - const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; + const reviverSeed = allHeldItems[HeldItemId.REVIVER_SEED] as InstantReviveHeldItem; vi.spyOn(reviverSeed, "apply"); game.move.select(MoveId.TACKLE); @@ -70,7 +71,7 @@ describe("Items - Reviver Seed", () => { player.damageAndUpdate(player.hp - 1); player.addTag(BattlerTagType.CONFUSED, 3); - const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; + const reviverSeed = allHeldItems[HeldItemId.REVIVER_SEED] as InstantReviveHeldItem; vi.spyOn(reviverSeed, "apply"); vi.spyOn(player, "randBattleSeedInt").mockReturnValue(0); // Force confusion self-hit @@ -122,8 +123,8 @@ describe("Items - Reviver Seed", () => { const player = game.scene.getPlayerPokemon()!; player.damageAndUpdate(player.hp - 1); - const playerSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; - vi.spyOn(playerSeed, "apply"); + const reviverSeed = allHeldItems[HeldItemId.REVIVER_SEED] as InstantReviveHeldItem; + vi.spyOn(reviverSeed, "apply"); game.move.select(move); await game.phaseInterceptor.to("TurnEndPhase"); diff --git a/test/items/scope-lens.test.ts b/test/items/scope-lens.test.ts index 578b0576aaa..3b0ef89f508 100644 --- a/test/items/scope-lens.test.ts +++ b/test/items/scope-lens.test.ts @@ -1,3 +1,4 @@ +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { TurnEndPhase } from "#phases/turn-end-phase"; @@ -26,7 +27,7 @@ describe("Items - Scope Lens", () => { .enemySpecies(SpeciesId.MAGIKARP) .enemyMoveset(MoveId.SPLASH) .moveset([MoveId.POUND]) - .startingHeldItems([{ name: "SCOPE_LENS" }]) + .startingHeldItems([{ entry: HeldItemId.SCOPE_LENS }]) .battleStyle("single"); }); diff --git a/test/items/temp-stat-stage-booster.test.ts b/test/items/temp-stat-stage-booster.test.ts index 806ff20df6c..de51c1612b6 100644 --- a/test/items/temp-stat-stage-booster.test.ts +++ b/test/items/temp-stat-stage-booster.test.ts @@ -4,11 +4,11 @@ import { MoveId } from "#enums/move-id"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; import { SpeciesId } from "#enums/species-id"; import { BATTLE_STATS, Stat } from "#enums/stat"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; -import { TempStatStageBoosterModifier } from "#modifiers/modifier"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; -import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import type { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -35,7 +35,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { .enemyMoveset(MoveId.SPLASH) .enemyAbility(AbilityId.BALL_FETCH) .moveset([MoveId.TACKLE, MoveId.SPLASH, MoveId.HONE_CLAWS, MoveId.BELLY_DRUM]) - .startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]); + .startingTrainerItems([{ entry: TrainerItemId.X_ATTACK }]); }); it("should provide a x1.3 stat stage multiplier", async () => { @@ -53,7 +53,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { }); it("should increase existing ACC stat stage by 1 for X_ACCURACY only", async () => { - game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }]).ability(AbilityId.SIMPLE); + game.override.startingTrainerItems([{ entry: TrainerItemId.X_ACCURACY }]).ability(AbilityId.SIMPLE); await game.classicMode.startBattle([SpeciesId.PIKACHU]); @@ -95,10 +95,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { }); it("should not increase past maximum stat stage multiplier", async () => { - game.override.startingModifier([ - { name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }, - { name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }, - ]); + game.override.startingTrainerItems([{ entry: TrainerItemId.X_ATTACK }, { entry: TrainerItemId.X_ACCURACY }]); await game.classicMode.startBattle([SpeciesId.PIKACHU]); @@ -129,17 +126,14 @@ describe("Items - Temporary Stat Stage Boosters", () => { await game.phaseInterceptor.to("BattleEndPhase"); - const modifier = game.scene.findModifier( - m => m instanceof TempStatStageBoosterModifier, - ) as TempStatStageBoosterModifier; - expect(modifier.getBattleCount()).toBe(4); + expect(game.scene.trainerItems.getStack(TrainerItemId.X_ATTACK)).toBe(4); // Forced X_ATTACK to spawn in the first slot with override game.onNextPrompt( - "SelectModifierPhase", - UiMode.MODIFIER_SELECT, + "SelectRewardPhase", + UiMode.REWARD_SELECT, () => { - const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler; + const handler = game.scene.ui.getHandler() as RewardSelectUiHandler; // Traverse to first modifier slot handler.setCursor(0); handler.setRowCursor(ShopCursorTarget.REWARDS); @@ -152,14 +146,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { await game.phaseInterceptor.to("TurnInitPhase"); // Making sure only one booster is in the modifier list even after picking up another - let count = 0; - for (const m of game.scene.modifiers) { - if (m instanceof TempStatStageBoosterModifier) { - count++; - const modifierInstance = m as TempStatStageBoosterModifier; - expect(modifierInstance.getBattleCount()).toBe(modifierInstance.getMaxBattles()); - } - } - expect(count).toBe(1); + + expect(game.scene.trainerItems.getStack(TrainerItemId.X_ATTACK)).toBe(5); }); }); diff --git a/test/items/thick-club.test.ts b/test/items/thick-club.test.ts index c497cef6338..1e7236d616d 100644 --- a/test/items/thick-club.test.ts +++ b/test/items/thick-club.test.ts @@ -1,7 +1,8 @@ -import { modifierTypes } from "#data/data-lists"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import i18next from "#plugins/i18n"; import { GameManager } from "#test/test-utils/game-manager"; import { NumberHolder, randInt } from "#utils/common"; @@ -29,7 +30,7 @@ describe("Items - Thick Club", () => { }); it("THICK_CLUB activates in battle correctly", async () => { - game.override.startingHeldItems([{ name: "RARE_SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]); + game.override.startingHeldItems([{ entry: HeldItemId.THICK_CLUB }]); const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.CUBONE]); @@ -90,16 +91,13 @@ describe("Items - Thick Club", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["THICK_CLUB"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + partyMember.heldItemManager.add(HeldItemId.THICK_CLUB); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(2); }); @@ -113,16 +111,13 @@ describe("Items - Thick Club", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["THICK_CLUB"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + partyMember.heldItemManager.add(HeldItemId.THICK_CLUB); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(2); }); @@ -136,16 +131,13 @@ describe("Items - Thick Club", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["THICK_CLUB"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + partyMember.heldItemManager.add(HeldItemId.THICK_CLUB); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(2); }); @@ -173,16 +165,13 @@ describe("Items - Thick Club", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["THICK_CLUB"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + partyMember.heldItemManager.add(HeldItemId.THICK_CLUB); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(2); }); @@ -210,16 +199,13 @@ describe("Items - Thick Club", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["THICK_CLUB"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + partyMember.heldItemManager.add(HeldItemId.THICK_CLUB); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(2); }); @@ -233,16 +219,13 @@ describe("Items - Thick Club", () => { // Making sure modifier is not applied without holding item const atkValue = new NumberHolder(atkStat); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - await game.scene.addModifier( - modifierTypes.RARE_SPECIES_STAT_BOOSTER().generateType([], ["THICK_CLUB"])!.newModifier(partyMember), - true, - ); - game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); + partyMember.heldItemManager.add(HeldItemId.THICK_CLUB); + applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: partyMember, stat: Stat.ATK, statValue: atkValue }); expect(atkValue.value / atkStat).toBe(1); }); diff --git a/test/items/toxic-orb.test.ts b/test/items/toxic-orb.test.ts index a1888a6aa1d..9626fdf6ee8 100644 --- a/test/items/toxic-orb.test.ts +++ b/test/items/toxic-orb.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; @@ -30,11 +31,7 @@ describe("Items - Toxic orb", () => { .enemyAbility(AbilityId.BALL_FETCH) .moveset(MoveId.SPLASH) .enemyMoveset(MoveId.SPLASH) - .startingHeldItems([ - { - name: "TOXIC_ORB", - }, - ]); + .startingHeldItems([{ entry: HeldItemId.TOXIC_ORB }]); vi.spyOn(i18next, "t"); }); @@ -43,7 +40,7 @@ describe("Items - Toxic orb", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); const player = game.scene.getPlayerPokemon()!; - expect(player.getHeldItems()[0].type.id).toBe("TOXIC_ORB"); + expect(player.heldItemManager.hasItem(HeldItemId.TOXIC_ORB)).toBe(true); game.move.select(MoveId.SPLASH); diff --git a/test/moves/beak-blast.test.ts b/test/moves/beak-blast.test.ts index 71d2d957bed..65c133eeed1 100644 --- a/test/moves/beak-blast.test.ts +++ b/test/moves/beak-blast.test.ts @@ -1,5 +1,6 @@ import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; @@ -87,7 +88,7 @@ describe("Moves - Beak Blast", () => { }); it("should only hit twice with Multi-Lens", async () => { - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); + game.override.startingHeldItems([{ entry: HeldItemId.MULTI_LENS }]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); diff --git a/test/moves/ceaseless-edge.test.ts b/test/moves/ceaseless-edge.test.ts index 56d7c97ea68..76a37ec5d58 100644 --- a/test/moves/ceaseless-edge.test.ts +++ b/test/moves/ceaseless-edge.test.ts @@ -3,6 +3,7 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { MoveEffectPhase } from "#phases/move-effect-phase"; @@ -61,7 +62,7 @@ describe("Moves - Ceaseless Edge", () => { }); test("move should hit twice with multi lens and apply two layers of spikes", async () => { - game.override.startingHeldItems([{ name: "MULTI_LENS" }]); + game.override.startingHeldItems([{ entry: HeldItemId.MULTI_LENS }]); await game.classicMode.startBattle([SpeciesId.ILLUMISE]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -83,7 +84,7 @@ describe("Moves - Ceaseless Edge", () => { }); test("trainer - move should hit twice, apply two layers of spikes, force switch opponent - opponent takes damage", async () => { - game.override.startingHeldItems([{ name: "MULTI_LENS" }]).startingWave(25); + game.override.startingHeldItems([{ entry: HeldItemId.MULTI_LENS }]).startingWave(25); await game.classicMode.startBattle([SpeciesId.ILLUMISE]); diff --git a/test/moves/destiny-bond.test.ts b/test/moves/destiny-bond.test.ts index 48bd29fe662..4ffc3734309 100644 --- a/test/moves/destiny-bond.test.ts +++ b/test/moves/destiny-bond.test.ts @@ -4,10 +4,10 @@ import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerIndex } from "#enums/battler-index"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; -import { PokemonInstantReviveModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -232,7 +232,7 @@ describe("Moves - Destiny Bond", () => { it("should not allow the opponent to revive via Reviver Seed", async () => { const moveToUse = MoveId.TACKLE; - game.override.moveset(moveToUse).startingHeldItems([{ name: "REVIVER_SEED" }]); + game.override.moveset(moveToUse).startingHeldItems([{ entry: HeldItemId.REVIVER_SEED }]); await game.classicMode.startBattle(defaultParty); const enemyPokemon = game.scene.getEnemyPokemon(); @@ -246,9 +246,6 @@ describe("Moves - Destiny Bond", () => { expect(playerPokemon?.isFainted()).toBe(true); // Check that the Tackle user's Reviver Seed did not activate - const revSeeds = game.scene - .getModifiers(PokemonInstantReviveModifier) - .filter(m => m.pokemonId === playerPokemon?.id); - expect(revSeeds.length).toBe(1); + expect(playerPokemon?.heldItemManager.getStack(HeldItemId.REVIVER_SEED)).toBe(1); }); }); diff --git a/test/moves/dragon-tail.test.ts b/test/moves/dragon-tail.test.ts index 487647784f7..c762da63265 100644 --- a/test/moves/dragon-tail.test.ts +++ b/test/moves/dragon-tail.test.ts @@ -3,6 +3,7 @@ import { Status } from "#data/status-effect"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { Challenges } from "#enums/challenges"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; @@ -162,7 +163,7 @@ describe("Moves - Dragon Tail", () => { it("should not cause a softlock when activating an opponent trainer's reviver seed", async () => { game.override .startingWave(5) - .enemyHeldItems([{ name: "REVIVER_SEED" }]) + .enemyHeldItems([{ entry: HeldItemId.REVIVER_SEED }]) .startingLevel(1000); // To make sure Dragon Tail KO's the opponent await game.classicMode.startBattle([SpeciesId.DRATINI]); @@ -179,7 +180,7 @@ describe("Moves - Dragon Tail", () => { it("should not cause a softlock when activating a player's reviver seed", async () => { game.override - .startingHeldItems([{ name: "REVIVER_SEED" }]) + .startingHeldItems([{ entry: HeldItemId.REVIVER_SEED }]) .enemyMoveset(MoveId.DRAGON_TAIL) .enemyLevel(1000); // To make sure Dragon Tail KO's the player await game.classicMode.startBattle([SpeciesId.DRATINI, SpeciesId.BULBASAUR]); diff --git a/test/moves/electro-shot.test.ts b/test/moves/electro-shot.test.ts index e5031fefb3d..5e531253f4d 100644 --- a/test/moves/electro-shot.test.ts +++ b/test/moves/electro-shot.test.ts @@ -1,5 +1,6 @@ import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; @@ -87,7 +88,7 @@ describe("Moves - Electro Shot", () => { }); it("should only increase Sp. Atk once with Multi-Lens", async () => { - game.override.weather(WeatherType.RAIN).startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); + game.override.weather(WeatherType.RAIN).startingHeldItems([{ entry: HeldItemId.MULTI_LENS }]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); diff --git a/test/moves/fell-stinger.test.ts b/test/moves/fell-stinger.test.ts index 9f482202c47..1eab2be4f4e 100644 --- a/test/moves/fell-stinger.test.ts +++ b/test/moves/fell-stinger.test.ts @@ -1,5 +1,6 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -88,7 +89,7 @@ describe("Moves - Fell Stinger", () => { }); it("should not grant stat boost if enemy is saved by Reviver Seed", async () => { - game.override.enemyAbility(AbilityId.BALL_FETCH).enemyHeldItems([{ name: "REVIVER_SEED" }]); + game.override.enemyAbility(AbilityId.BALL_FETCH).enemyHeldItems([{ entry: HeldItemId.REVIVER_SEED }]); await game.classicMode.startBattle([SpeciesId.LEAVANNY]); const leadPokemon = game.scene.getPlayerPokemon()!; diff --git a/test/moves/fusion-flare-bolt.test.ts b/test/moves/fusion-flare-bolt.test.ts index 42cc1248325..0bba6765cf6 100644 --- a/test/moves/fusion-flare-bolt.test.ts +++ b/test/moves/fusion-flare-bolt.test.ts @@ -164,8 +164,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { const enemyParty = game.scene.getEnemyParty(); // Get rid of any modifiers that may alter power - game.scene.clearEnemyHeldItemModifiers(); - game.scene.clearEnemyModifiers(); + game.scene.clearEnemyItems(); // Mock stats by replacing entries in copy with desired values for specific stats const stats = { @@ -218,8 +217,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { const enemyParty = game.scene.getEnemyParty(); // Get rid of any modifiers that may alter power - game.scene.clearEnemyHeldItemModifiers(); - game.scene.clearEnemyModifiers(); + game.scene.clearEnemyItems(); // Mock stats by replacing entries in copy with desired values for specific stats const stats = { diff --git a/test/moves/glaive-rush.test.ts b/test/moves/glaive-rush.test.ts index f20abd68500..5c2eb59145e 100644 --- a/test/moves/glaive-rush.test.ts +++ b/test/moves/glaive-rush.test.ts @@ -1,5 +1,6 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { GameManager } from "#test/test-utils/game-manager"; @@ -60,7 +61,7 @@ describe("Moves - Glaive Rush", () => { }); it("interacts properly with multi-lens", async () => { - game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }]).enemyMoveset([MoveId.AVALANCHE]); + game.override.startingHeldItems([{ entry: HeldItemId.MULTI_LENS, count: 2 }]).enemyMoveset([MoveId.AVALANCHE]); await game.classicMode.startBattle([SpeciesId.KLINK]); const player = game.scene.getPlayerPokemon()!; diff --git a/test/moves/heal-block.test.ts b/test/moves/heal-block.test.ts index fc814fda4bc..ad950fc0fba 100644 --- a/test/moves/heal-block.test.ts +++ b/test/moves/heal-block.test.ts @@ -1,6 +1,7 @@ import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { PositionalTagType } from "#enums/positional-tag-type"; import { SpeciesId } from "#enums/species-id"; @@ -134,7 +135,7 @@ describe("Moves - Heal Block", () => { }); it("should stop healing from items", async () => { - game.override.startingHeldItems([{ name: "LEFTOVERS" }]); + game.override.startingHeldItems([{ entry: HeldItemId.LEFTOVERS }]); await game.classicMode.startBattle([SpeciesId.CHARIZARD]); diff --git a/test/moves/payback.test.ts b/test/moves/payback.test.ts index f9692bd480f..9e156970dfe 100644 --- a/test/moves/payback.test.ts +++ b/test/moves/payback.test.ts @@ -33,7 +33,7 @@ describe("Move - Payback", () => { .enemySpecies(SpeciesId.DRACOVISH) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) - .startingModifier([{ name: "POKEBALL", count: 5 }]); + .startingPokeballs({ [PokeballType.POKEBALL]: 5 }); powerSpy = vi.spyOn(allMoves[MoveId.PAYBACK], "calculateBattlePower"); }); diff --git a/test/moves/safeguard.test.ts b/test/moves/safeguard.test.ts index 19b56cd759f..1c493a059d1 100644 --- a/test/moves/safeguard.test.ts +++ b/test/moves/safeguard.test.ts @@ -1,6 +1,7 @@ import { allAbilities } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; @@ -113,7 +114,7 @@ describe("Moves - Safeguard", () => { }); it("doesn't protect from self-inflicted status from Rest or Flame Orb", async () => { - game.override.enemyHeldItems([{ name: "FLAME_ORB" }]); + game.override.enemyHeldItems([{ entry: HeldItemId.FLAME_ORB }]); await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; enemyPokemon.hp = 1; diff --git a/test/moves/substitute.test.ts b/test/moves/substitute.test.ts index 15fd770805a..c3e0d7f47c6 100644 --- a/test/moves/substitute.test.ts +++ b/test/moves/substitute.test.ts @@ -5,8 +5,8 @@ import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; import { Command } from "#enums/command"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; @@ -296,7 +296,7 @@ describe("Moves - Substitute", () => { }); it("should prevent the user's items from being stolen", async () => { - game.override.enemyMoveset(MoveId.THIEF).startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); + game.override.enemyMoveset(MoveId.THIEF).startingHeldItems([{ entry: HeldItemId.SITRUS_BERRY }]); vi.spyOn(allMoves[MoveId.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -313,7 +313,7 @@ describe("Moves - Substitute", () => { }); it("should prevent the user's items from being removed", async () => { - game.override.moveset([MoveId.KNOCK_OFF]).enemyHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); + game.override.moveset([MoveId.KNOCK_OFF]).enemyHeldItems([{ entry: HeldItemId.SITRUS_BERRY }]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); @@ -330,7 +330,7 @@ describe("Moves - Substitute", () => { }); it("move effect should prevent the user's berries from being stolen and eaten", async () => { - game.override.enemyMoveset(MoveId.BUG_BITE).startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); + game.override.enemyMoveset(MoveId.BUG_BITE).startingHeldItems([{ entry: HeldItemId.SITRUS_BERRY }]); await game.classicMode.startBattle([SpeciesId.BLASTOISE]); diff --git a/test/moves/tera-blast.test.ts b/test/moves/tera-blast.test.ts index 37dd8f53eaf..4e46602cfcc 100644 --- a/test/moves/tera-blast.test.ts +++ b/test/moves/tera-blast.test.ts @@ -1,6 +1,7 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; @@ -141,9 +142,7 @@ describe("Moves - Tera Blast", () => { }); it("does not change its move category from stat changes due to held items", async () => { - game.override - .startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]) - .starterSpecies(SpeciesId.CUBONE); + game.override.startingHeldItems([{ entry: HeldItemId.THICK_CLUB }]).starterSpecies(SpeciesId.CUBONE); await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; diff --git a/test/moves/transform-imposter.test.ts b/test/moves/transform-imposter.test.ts index b1631130154..29c4f7c2e11 100644 --- a/test/moves/transform-imposter.test.ts +++ b/test/moves/transform-imposter.test.ts @@ -3,7 +3,7 @@ import { AbilityId } from "#enums/ability-id"; import { BattleType } from "#enums/battle-type"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { PokemonType } from "#enums/pokemon-type"; @@ -128,7 +128,7 @@ describe("Transforming Effects", () => { it.todo("should copy the target's rage fist hit count"); it("should not copy friendship, held items, nickname, level or non-volatile status effects", async () => { - game.override.enemyHeldItems([{ name: "BERRY", count: 1, type: BerryType.SITRUS }]); + game.override.enemyHeldItems([{ entry: HeldItemId.SITRUS_BERRY }]); await game.classicMode.startBattle([SpeciesId.DITTO]); const ditto = game.field.getPlayerPokemon(); diff --git a/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts b/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts index 93cf4537c53..a9214703b15 100644 --- a/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts +++ b/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts @@ -11,7 +11,7 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; import { PartyHealPhase } from "#phases/party-heal-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, @@ -130,8 +130,8 @@ describe("A Trainer's Test - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); const eggsAfter = scene.gameData.eggs; expect(eggsAfter).toBeDefined(); @@ -178,8 +178,8 @@ describe("A Trainer's Test - Mystery Encounter", () => { const eggsBeforeLength = eggsBefore.length; await runMysteryEncounterToEnd(game, 2); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); const eggsAfter = scene.gameData.eggs; expect(eggsAfter).toBeDefined(); diff --git a/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index 562482dd520..81ea15a9f96 100644 --- a/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -1,24 +1,22 @@ import type { BattleScene } from "#app/battle-scene"; -import { BerryType } from "#enums/berry-type"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; -import { BerryModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; import { AbsoluteAvariceEncounter } from "#mystery-encounters/absolute-avarice-encounter"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; import { MovePhase } from "#phases/move-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import i18next from "i18next"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/absoluteAvarice"; @@ -77,8 +75,6 @@ describe("Absolute Avarice - Mystery Encounter", () => { }); it("should not spawn if player does not have enough berries", async () => { - scene.modifiers = []; - await game.runToMysteryEncounter(); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); @@ -86,9 +82,9 @@ describe("Absolute Avarice - Mystery Encounter", () => { it("should spawn if player has enough berries", async () => { game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT).startingHeldItems([ - { name: "BERRY", count: 2, type: BerryType.SITRUS }, - { name: "BERRY", count: 3, type: BerryType.GANLON }, - { name: "BERRY", count: 2, type: BerryType.APICOT }, + { entry: HeldItemId.SITRUS_BERRY, count: 2 }, + { entry: HeldItemId.GANLON_BERRY, count: 3 }, + { entry: HeldItemId.APICOT_BERRY, count: 2 }, ]); await game.runToMysteryEncounter(); @@ -98,15 +94,15 @@ describe("Absolute Avarice - Mystery Encounter", () => { it("should remove all player's berries at the start of the encounter", async () => { game.override.startingHeldItems([ - { name: "BERRY", count: 2, type: BerryType.SITRUS }, - { name: "BERRY", count: 3, type: BerryType.GANLON }, - { name: "BERRY", count: 2, type: BerryType.APICOT }, + { entry: HeldItemId.SITRUS_BERRY, count: 2 }, + { entry: HeldItemId.GANLON_BERRY, count: 3 }, + { entry: HeldItemId.APICOT_BERRY, count: 2 }, ]); await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); - expect(scene.modifiers?.length).toBe(0); + expect(scene.getPlayerParty()[0].getHeldItems().length).toBe(0); }); describe("Option 1 - Fight the Greedent", () => { @@ -147,20 +143,11 @@ describe("Absolute Avarice - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); for (const partyPokemon of scene.getPlayerParty()) { - const pokemonId = partyPokemon.id; - const pokemonItems = scene.findModifiers( - m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId, - true, - ) as PokemonHeldItemModifier[]; - const revSeed = pokemonItems.find( - i => i.type.name === i18next.t("modifierType:ModifierType.REVIVER_SEED.name"), - ); - expect(revSeed).toBeDefined; - expect(revSeed?.stackCount).toBe(1); + expect(partyPokemon.heldItemManager.getStack(HeldItemId.REVIVER_SEED)).toBe(1); } }); }); @@ -183,42 +170,36 @@ describe("Absolute Avarice - Mystery Encounter", () => { it("Should return 3 (2/5ths floored) berries if 8 were stolen", { retry: 5 }, async () => { game.override.startingHeldItems([ - { name: "BERRY", count: 2, type: BerryType.SITRUS }, - { name: "BERRY", count: 3, type: BerryType.GANLON }, - { name: "BERRY", count: 3, type: BerryType.APICOT }, + { entry: HeldItemId.SITRUS_BERRY, count: 2 }, + { entry: HeldItemId.GANLON_BERRY, count: 3 }, + { entry: HeldItemId.APICOT_BERRY, count: 3 }, ]); await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); - expect(scene.modifiers?.length).toBe(0); + expect(scene.getPlayerParty()[0].getHeldItems().length).toBe(0); await runMysteryEncounterToEnd(game, 2); - const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier); - const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0); - expect(berriesAfter).toBeDefined(); - expect(berryCountAfter).toBe(3); + expect(scene.getPlayerParty()[0].heldItemManager.getHeldItemCount()).toBe(3); }); it("Should return 2 (2/5ths floored) berries if 7 were stolen", { retry: 5 }, async () => { game.override.startingHeldItems([ - { name: "BERRY", count: 2, type: BerryType.SITRUS }, - { name: "BERRY", count: 3, type: BerryType.GANLON }, - { name: "BERRY", count: 2, type: BerryType.APICOT }, + { entry: HeldItemId.SITRUS_BERRY, count: 2 }, + { entry: HeldItemId.GANLON_BERRY, count: 3 }, + { entry: HeldItemId.APICOT_BERRY, count: 2 }, ]); await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); - expect(scene.modifiers?.length).toBe(0); + expect(scene.getPlayerParty()[0].getHeldItems().length).toBe(0); await runMysteryEncounterToEnd(game, 2); - const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier); - const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0); - expect(berriesAfter).toBeDefined(); - expect(berryCountAfter).toBe(2); + expect(scene.getPlayerParty()[0].heldItemManager.getHeldItemCount()).toBe(2); }); it("should leave encounter without battle", async () => { diff --git a/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index d903568785a..cefc17a1cb3 100644 --- a/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -6,13 +6,13 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; -import { ShinyRateBoosterModifier } from "#modifiers/modifier"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { PokemonMove } from "#moves/pokemon-move"; import { AnOfferYouCantRefuseEncounter } from "#mystery-encounters/an-offer-you-cant-refuse-encounter"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#mystery-encounters/mystery-encounters"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; @@ -144,10 +144,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); await runMysteryEncounterToEnd(game, 1); - const itemModifier = scene.findModifier(m => m instanceof ShinyRateBoosterModifier) as ShinyRateBoosterModifier; - - expect(itemModifier).toBeDefined(); - expect(itemModifier?.stackCount).toBe(1); + expect(scene.trainerItems.getStack(TrainerItemId.SHINY_CHARM)).toBe(1); }); it("Should remove the Pokemon from the party", async () => { @@ -197,7 +194,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { const expBefore = gyarados.exp; await runMysteryEncounterToEnd(game, 2); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to(SelectRewardPhase, false); expect(gyarados.exp).toBe( expBefore + Math.floor((getPokemonSpecies(SpeciesId.LIEPARD).baseExp * defaultWave) / 5 + 1), @@ -213,7 +210,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { const expBefore = abra.exp; await runMysteryEncounterToEnd(game, 2); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to(SelectRewardPhase, false); expect(abra.exp).toBe( expBefore + Math.floor((getPokemonSpecies(SpeciesId.LIEPARD).baseExp * defaultWave) / 5 + 1), diff --git a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 25116a89ec5..26fb0a71946 100644 --- a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -6,20 +6,21 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { BerryModifier } from "#modifiers/modifier"; +import type { HeldItemSpecs } from "#items/held-item-data-types"; +import { getPartyBerries } from "#items/item-utility"; import { BerriesAboundEncounter } from "#mystery-encounters/berries-abound-encounter"; import * as EncounterDialogueUtils from "#mystery-encounters/encounter-dialogue-utils"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/berriesAbound"; @@ -45,7 +46,6 @@ describe("Berries Abound - Mystery Encounter", () => { .startingWave(defaultWave) .startingBiome(defaultBiome) .disableTrainerWaves() - .startingModifier([]) .startingHeldItems([]) .enemyAbility(AbilityId.BALL_FETCH) .enemyPassiveAbility(AbilityId.BALL_FETCH); @@ -128,17 +128,15 @@ describe("Berries Abound - Mystery Encounter", () => { const numBerries = game.scene.currentBattle.mysteryEncounter!.misc.numBerries; // Clear out any pesky mods that slipped through test spin-up - scene.modifiers.forEach(mod => { - scene.removeModifier(mod); - }); + scene.clearAllItems(); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); - const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; - const berriesAfterCount = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + const berriesAfter = getPartyBerries(); + const berriesAfterCount = berriesAfter.reduce((a, b) => a + (b.item as HeldItemSpecs).stack, 0); expect(numBerries).toBe(berriesAfterCount); }); @@ -147,17 +145,17 @@ describe("Berries Abound - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - for (const option of modifierSelectHandler.options) { - expect(option.modifierTypeOption.type.id).toContain("BERRY"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + for (const option of rewardSelectHandler.options) { + expect(option.rewardOption.type.id).toContain("BERRY"); } }); }); @@ -232,17 +230,17 @@ describe("Berries Abound - Mystery Encounter", () => { }); await runMysteryEncounterToEnd(game, 2); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - for (const option of modifierSelectHandler.options) { - expect(option.modifierTypeOption.type.id).toContain("BERRY"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + for (const option of rewardSelectHandler.options) { + expect(option.rewardOption.type.id).toContain("BERRY"); } expect(EncounterDialogueUtils.showEncounterText).toHaveBeenCalledWith(`${namespace}:option.2.selected`); diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index bed9d48d063..589d9a7b3ad 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -1,5 +1,6 @@ import type { BattleScene } from "#app/battle-scene"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -7,14 +8,13 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { UiMode } from "#enums/ui-mode"; -import { ContactHeldItemTransferChanceModifier } from "#modifiers/modifier"; import { PokemonMove } from "#moves/pokemon-move"; import { BugTypeSuperfanEncounter } from "#mystery-encounters/bug-type-superfan-encounter"; import * as encounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#phases/mystery-encounter-phases"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, @@ -22,7 +22,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/bugTypeSuperfan"; @@ -412,43 +412,43 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); }); - it("should proceed to rewards screen with 0-1 Bug Types reward options", async () => { + it("should proceed to allRewards screen with 0-1 Bug Types reward options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); await runMysteryEncounterToEnd(game, 2); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(2); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("SUPER_LURE"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("GREAT_BALL"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(2); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toBe("SUPER_LURE"); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toBe("GREAT_BALL"); }); - it("should proceed to rewards screen with 2-3 Bug Types reward options", async () => { + it("should proceed to allRewards screen with 2-3 Bug Types reward options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [ SpeciesId.BUTTERFREE, SpeciesId.BEEDRILL, ]); await runMysteryEncounterToEnd(game, 2); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("QUICK_CLAW"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("ULTRA_BALL"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toBe("QUICK_CLAW"); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toBe("MAX_LURE"); + expect(rewardSelectHandler.options[2].rewardOption.type.id).toBe("ULTRA_BALL"); }); - it("should proceed to rewards screen with 4-5 Bug Types reward options", async () => { + it("should proceed to allRewards screen with 4-5 Bug Types reward options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [ SpeciesId.BUTTERFREE, SpeciesId.BEEDRILL, @@ -457,20 +457,20 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { ]); await runMysteryEncounterToEnd(game, 2); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("GRIP_CLAW"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("ROGUE_BALL"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toBe("GRIP_CLAW"); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toBe("MAX_LURE"); + expect(rewardSelectHandler.options[2].rewardOption.type.id).toBe("ROGUE_BALL"); }); - it("should proceed to rewards screen with 6 Bug Types reward options (including form change item)", async () => { + it("should proceed to allRewards screen with 6 Bug Types reward options (including form change item)", async () => { await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [ SpeciesId.BUTTERFREE, SpeciesId.BEEDRILL, @@ -481,18 +481,18 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { ]); await runMysteryEncounterToEnd(game, 2); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(4); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MASTER_BALL"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MEGA_BRACELET"); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("DYNAMAX_BAND"); - expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toBe("FORM_CHANGE_ITEM"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(4); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toBe("MASTER_BALL"); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toBe("MEGA_BRACELET"); + expect(rewardSelectHandler.options[2].rewardOption.type.id).toBe("DYNAMAX_BAND"); + expect(rewardSelectHandler.options[3].rewardOption.type.id).toBe("FORM_CHANGE_ITEM"); }); it("should leave encounter without battle", async () => { @@ -528,11 +528,11 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { }); it("should NOT be selectable if the player doesn't have any Bug items", async () => { - game.scene.modifiers = []; + game.scene.trainerItems.clearItems(); await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); await game.phaseInterceptor.to(MysteryEncounterPhase, false); - game.scene.modifiers = []; + game.scene.trainerItems.clearItems(); const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; @@ -548,33 +548,31 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); }); - it("should remove the gifted item and proceed to rewards screen", async () => { - game.override.startingHeldItems([{ name: "GRIP_CLAW", count: 1 }]); + it("should remove the gifted item and proceed to allRewards screen", async () => { + game.override.startingHeldItems([{ entry: HeldItemId.GRIP_CLAW, count: 1 }]); await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [SpeciesId.BUTTERFREE]); - const gripClawCountBefore = - scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0; + const gripClawCountBefore = scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.GRIP_CLAW); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(2); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MYSTERY_ENCOUNTER_GOLDEN_BUG_NET"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("REVIVER_SEED"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(2); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toBe("MYSTERY_ENCOUNTER_GOLDEN_BUG_NET"); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toBe("REVIVER_SEED"); - const gripClawCountAfter = - scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0; + const gripClawCountAfter = scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.GRIP_CLAW); expect(gripClawCountBefore - 1).toBe(gripClawCountAfter); }); it("should leave encounter without battle", async () => { - game.override.startingHeldItems([{ name: "GRIP_CLAW", count: 1 }]); + game.override.startingHeldItems([{ entry: HeldItemId.GRIP_CLAW, count: 1 }]); const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [SpeciesId.BUTTERFREE]); diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index b573701d568..72c69ac36e3 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -1,32 +1,28 @@ import type { BattleScene } from "#app/battle-scene"; import * as BattleAnims from "#data/battle-anims"; -import { modifierTypes } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; -import { BerryType } from "#enums/berry-type"; import { BiomeId } from "#enums/biome-id"; import { Button } from "#enums/buttons"; -import { ModifierTier } from "#enums/modifier-tier"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PokemonType } from "#enums/pokemon-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { UiMode } from "#enums/ui-mode"; -import type { Pokemon } from "#field/pokemon"; -import type { PokemonHeldItemModifier } from "#modifiers/modifier"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; +import { getHeldItemTier } from "#items/held-item-default-tiers"; import { PokemonMove } from "#moves/pokemon-move"; import { ClowningAroundEncounter } from "#mystery-encounters/clowning-around-encounter"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; -import { generateModifierType } from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; import { MovePhase } from "#phases/move-phase"; import { PostMysteryEncounterPhase } from "#phases/mystery-encounter-phases"; import { NewBattlePhase } from "#phases/new-battle-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, @@ -200,9 +196,9 @@ describe("Clowning Around - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); const abilityToTrain = scene.currentBattle.mysteryEncounter?.misc.ability; game.onNextPrompt("PostMysteryEncounterPhase", UiMode.MESSAGE, () => { @@ -266,48 +262,32 @@ describe("Clowning Around - Mystery Encounter", () => { scene.getPlayerParty()[0].moveset = [new PokemonMove(MoveId.TACKLE), new PokemonMove(MoveId.THIEF)]; // 2 Sitrus Berries on lead - scene.modifiers = []; - let itemType = generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); - // 2 Ganlon Berries on lead - itemType = generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); - // 5 Golden Punch on lead (ultra) - itemType = generateModifierType(modifierTypes.GOLDEN_PUNCH) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); - // 5 Lucky Egg on lead (ultra) - itemType = generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); - // 3 Soothe Bell on lead (great tier, but counted as ultra by this ME) - itemType = generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 3, itemType); - // 5 Soul Dew on lead (rogue) - itemType = generateModifierType(modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); - // 2 Golden Egg on lead (rogue) - itemType = generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); + scene.clearAllItems(); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SITRUS_BERRY, 2); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.GANLON_BERRY, 2); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.GOLDEN_PUNCH, 5); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.LUCKY_EGG, 5); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOOTHE_BELL, 3); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOUL_DEW, 5); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.GOLDEN_EGG, 2); - // 5 Soul Dew on second party pokemon (these should not change) - itemType = generateModifierType(modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[1], 5, itemType); + scene.getPlayerParty()[1].heldItemManager.add(HeldItemId.SOUL_DEW, 5); await runMysteryEncounterToEnd(game, 2); const leadItemsAfter = scene.getPlayerParty()[0].getHeldItems(); const ultraCountAfter = leadItemsAfter - .filter(m => m.type.tier === ModifierTier.ULTRA) - .reduce((a, b) => a + b.stackCount, 0); + .filter(m => getHeldItemTier(m) === RarityTier.ULTRA) + .reduce((a, b) => a + scene.getPlayerParty()[0].heldItemManager.getStack(b), 0); const rogueCountAfter = leadItemsAfter - .filter(m => m.type.tier === ModifierTier.ROGUE) - .reduce((a, b) => a + b.stackCount, 0); + .filter(m => getHeldItemTier(m) === RarityTier.ROGUE) + .reduce((a, b) => a + scene.getPlayerParty()[0].heldItemManager.getStack(b), 0); expect(ultraCountAfter).toBe(13); expect(rogueCountAfter).toBe(7); const secondItemsAfter = scene.getPlayerParty()[1].getHeldItems(); expect(secondItemsAfter.length).toBe(1); - expect(secondItemsAfter[0].type.id).toBe("SOUL_DEW"); - expect(secondItemsAfter[0]?.stackCount).toBe(5); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SOUL_DEW)).toBe(5); }); it("should leave encounter without battle", async () => { @@ -381,15 +361,3 @@ describe("Clowning Around - Mystery Encounter", () => { }); }); }); - -async function addItemToPokemon( - scene: BattleScene, - pokemon: Pokemon, - stackCount: number, - itemType: PokemonHeldItemModifierType, -) { - const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier; - itemMod.stackCount = stackCount; - scene.addModifier(itemMod, true, false, false, true); - await scene.updateModifiers(true); -} diff --git a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts index 97d0ce31367..afbe2e30e59 100644 --- a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts +++ b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -14,14 +14,14 @@ import { CommandPhase } from "#phases/command-phase"; import { LearnMovePhase } from "#phases/learn-move-phase"; import { MovePhase } from "#phases/move-phase"; import { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase, } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/dancingLessons"; @@ -118,7 +118,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { expect(movePhases.filter(p => (p as MovePhase).move.moveId === MoveId.REVELATION_DANCE).length).toBe(1); // Revelation Dance used before battle }); - it("should have a Baton in the rewards after battle", async () => { + it("should have a Baton in the allRewards after battle", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); // Make party lead's level arbitrarily high to not get KOed by move const partyLead = scene.getPlayerParty()[0]; @@ -126,16 +126,16 @@ describe("Dancing Lessons - Mystery Encounter", () => { partyLead.calculateStats(); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); // Should fill remaining - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toContain("BATON"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); // Should fill remaining + expect(rewardSelectHandler.options[0].rewardOption.type.id).toContain("BATON"); }); }); diff --git a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 16c726f1de6..407a26d5483 100644 --- a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -1,24 +1,13 @@ import type { BattleScene } from "#app/battle-scene"; -import { modifierTypes } from "#data/data-lists"; -import { BerryType } from "#enums/berry-type"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; -import { - BerryModifier, - HealingBoosterModifier, - HitHealModifier, - LevelIncrementBoosterModifier, - MoneyMultiplierModifier, - PokemonInstantReviveModifier, - PokemonNatureWeightModifier, - PreserveBerryModifier, -} from "#modifiers/modifier"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { DelibirdyEncounter } from "#mystery-encounters/delibirdy-encounter"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; -import { generateModifierType } from "#mystery-encounters/encounter-phase-utils"; import type { MoneyRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; @@ -119,10 +108,7 @@ describe("Delibird-y - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await runMysteryEncounterToEnd(game, 1); - const itemModifier = scene.findModifier(m => m instanceof MoneyMultiplierModifier) as MoneyMultiplierModifier; - - expect(itemModifier).toBeDefined(); - expect(itemModifier?.stackCount).toBe(1); + expect(scene.trainerItems.getStack(TrainerItemId.AMULET_COIN)).toBe(1); }); it("Should give the player a Shell Bell if they have max stacks of Amulet Coins", async () => { @@ -130,21 +116,14 @@ describe("Delibird-y - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Max Amulet Coins - scene.modifiers = []; - const amuletCoin = generateModifierType(modifierTypes.AMULET_COIN)!.newModifier() as MoneyMultiplierModifier; - amuletCoin.stackCount = 5; - scene.addModifier(amuletCoin, true, false, false, true); - await scene.updateModifiers(true); + scene.clearAllItems(); + scene.trainerItems.add(TrainerItemId.AMULET_COIN, 5); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 1); - const amuletCoinAfter = scene.findModifier(m => m instanceof MoneyMultiplierModifier); - const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); - - expect(amuletCoinAfter).toBeDefined(); - expect(amuletCoinAfter?.stackCount).toBe(5); - expect(shellBellAfter).toBeDefined(); - expect(shellBellAfter?.stackCount).toBe(1); + expect(scene.trainerItems.getStack(TrainerItemId.AMULET_COIN)).toBe(5); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SHELL_BELL)).toBe(1); }); it("should be disabled if player does not have enough money", async () => { @@ -199,111 +178,73 @@ describe("Delibird-y - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 2 Sitrus berries on party lead - scene.modifiers = []; - const sitrus = generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS])!; - const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; - sitrusMod.stackCount = 2; - scene.addModifier(sitrusMod, true, false, false, true); - await scene.updateModifiers(true); + scene.clearAllItems(); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SITRUS_BERRY, 2); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); - const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier); - const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier); - - expect(sitrusAfter?.stackCount).toBe(1); - expect(candyJarAfter).toBeDefined(); - expect(candyJarAfter?.stackCount).toBe(1); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SITRUS_BERRY)).toBe(1); + expect(scene.trainerItems.getStack(TrainerItemId.CANDY_JAR)).toBe(1); }); it("Should remove Reviver Seed and give the player a Berry Pouch", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Reviver Seed on party lead - scene.modifiers = []; - const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; - modifier.stackCount = 1; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.clearAllItems(); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.REVIVER_SEED); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); - const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); - const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); - - expect(reviverSeedAfter).toBeUndefined(); - expect(berryPouchAfter).toBeDefined(); - expect(berryPouchAfter?.stackCount).toBe(1); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.REVIVER_SEED)).toBe(0); + expect(scene.trainerItems.getStack(TrainerItemId.BERRY_POUCH)).toBe(1); }); it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // 99 Candy Jars - scene.modifiers = []; - const candyJar = generateModifierType(modifierTypes.CANDY_JAR)!.newModifier() as LevelIncrementBoosterModifier; - candyJar.stackCount = 99; - scene.addModifier(candyJar, true, false, false, true); - const sitrus = generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS])!; + scene.clearAllItems(); + scene.trainerItems.add(TrainerItemId.CANDY_JAR, 99); // Sitrus berries on party - const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; - sitrusMod.stackCount = 2; - scene.addModifier(sitrusMod, true, false, false, true); - await scene.updateModifiers(true); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SITRUS_BERRY, 2); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); - const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier); - const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier); - const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); - - expect(sitrusAfter?.stackCount).toBe(1); - expect(candyJarAfter).toBeDefined(); - expect(candyJarAfter?.stackCount).toBe(99); - expect(shellBellAfter).toBeDefined(); - expect(shellBellAfter?.stackCount).toBe(1); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SITRUS_BERRY)).toBe(1); + expect(scene.trainerItems.getStack(TrainerItemId.CANDY_JAR)).toBe(99); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SHELL_BELL)).toBe(1); }); it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // 3 Berry Pouches - scene.modifiers = []; - const healingCharm = generateModifierType(modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; - healingCharm.stackCount = 3; - scene.addModifier(healingCharm, true, false, false, true); + scene.clearAllItems(); + scene.trainerItems.add(TrainerItemId.BERRY_POUCH, 3); // Set 1 Reviver Seed on party lead - const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; - modifier.stackCount = 1; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.REVIVER_SEED); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); - const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); - const healingCharmAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); - const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); - - expect(reviverSeedAfter).toBeUndefined(); - expect(healingCharmAfter).toBeDefined(); - expect(healingCharmAfter?.stackCount).toBe(3); - expect(shellBellAfter).toBeDefined(); - expect(shellBellAfter?.stackCount).toBe(1); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.REVIVER_SEED)).toBe(0); + expect(scene.trainerItems.getStack(TrainerItemId.BERRY_POUCH)).toBe(3); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SHELL_BELL)).toBe(1); }); it("should be disabled if player does not have any proper items", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Soul Dew on party lead - scene.modifiers = []; - const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]); - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.clearAllItems(); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOUL_DEW); + scene.updateItems(true); await game.phaseInterceptor.to(MysteryEncounterPhase, false); @@ -328,11 +269,8 @@ describe("Delibird-y - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Reviver Seed on party lead - const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; - modifier.stackCount = 1; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.REVIVER_SEED); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); @@ -361,82 +299,55 @@ describe("Delibird-y - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 2 Soul Dew on party lead - scene.modifiers = []; - const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; - modifier.stackCount = 2; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.clearAllItems(); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOUL_DEW, 2); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); - - expect(soulDewAfter?.stackCount).toBe(1); - expect(healingCharmAfter).toBeDefined(); - expect(healingCharmAfter?.stackCount).toBe(1); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SOUL_DEW)).toBe(1); + expect(scene.trainerItems.getStack(TrainerItemId.HEALING_CHARM)).toBe(1); }); it("Should remove held item and give the player a Healing Charm", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Soul Dew on party lead - scene.modifiers = []; - const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; - modifier.stackCount = 1; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.clearAllItems(); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOUL_DEW); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); - - expect(soulDewAfter).toBeUndefined(); - expect(healingCharmAfter).toBeDefined(); - expect(healingCharmAfter?.stackCount).toBe(1); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SOUL_DEW)).toBe(0); + expect(scene.trainerItems.getStack(TrainerItemId.HEALING_CHARM)).toBe(1); }); it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // 5 Healing Charms - scene.modifiers = []; - const healingCharm = generateModifierType(modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; - healingCharm.stackCount = 5; - scene.addModifier(healingCharm, true, false, false, true); + scene.clearAllItems(); + scene.trainerItems.add(TrainerItemId.HEALING_CHARM, 5); // Set 1 Soul Dew on party lead - const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; - modifier.stackCount = 1; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOUL_DEW); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); - const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); - - expect(soulDewAfter).toBeUndefined(); - expect(healingCharmAfter).toBeDefined(); - expect(healingCharmAfter?.stackCount).toBe(5); - expect(shellBellAfter).toBeDefined(); - expect(shellBellAfter?.stackCount).toBe(1); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SOUL_DEW)).toBe(0); + expect(scene.trainerItems.getStack(TrainerItemId.HEALING_CHARM)).toBe(5); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SHELL_BELL)).toBe(1); }); it("should be disabled if player does not have any proper items", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Reviver Seed on party lead - scene.modifiers = []; - const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getPlayerParty()[0]); - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.clearAllItems(); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.REVIVER_SEED); + scene.updateItems(true); await game.phaseInterceptor.to(MysteryEncounterPhase, false); @@ -461,12 +372,9 @@ describe("Delibird-y - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Soul Dew on party lead - scene.modifiers = []; - const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; - modifier.stackCount = 1; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.clearAllItems(); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOUL_DEW); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); diff --git a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index 3d84d70b47e..11b1cadd7e9 100644 --- a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -9,10 +9,10 @@ import { DepartmentStoreSaleEncounter } from "#mystery-encounters/department-sto import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CIVILIZATION_ENCOUNTER_BIOMES } from "#mystery-encounters/mystery-encounters"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/departmentStoreSale"; @@ -93,16 +93,16 @@ describe("Department Store Sale - Mystery Encounter", () => { it("should have shop with only TMs", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); await runMysteryEncounterToEnd(game, 1); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - for (const option of modifierSelectHandler.options) { - expect(option.modifierTypeOption.type.id).toContain("TM_"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + for (const option of rewardSelectHandler.options) { + expect(option.rewardOption.type.id).toContain("TM_"); } }); @@ -130,18 +130,17 @@ describe("Department Store Sale - Mystery Encounter", () => { it("should have shop with only Vitamins", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); await runMysteryEncounterToEnd(game, 2); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); - for (const option of modifierSelectHandler.options) { + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + for (const option of rewardSelectHandler.options) { expect( - option.modifierTypeOption.type.id.includes("PP_UP") || - option.modifierTypeOption.type.id.includes("BASE_STAT_BOOSTER"), + option.rewardOption.type.id.includes("PP_UP") || option.rewardOption.type.id.includes("BASE_STAT_BOOSTER"), ).toBeTruthy(); } }); @@ -170,18 +169,18 @@ describe("Department Store Sale - Mystery Encounter", () => { it("should have shop with only X Items", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); await runMysteryEncounterToEnd(game, 3); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - for (const option of modifierSelectHandler.options) { + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + for (const option of rewardSelectHandler.options) { expect( - option.modifierTypeOption.type.id.includes("DIRE_HIT") || - option.modifierTypeOption.type.id.includes("TEMP_STAT_STAGE_BOOSTER"), + option.rewardOption.type.id.includes("DIRE_HIT") || + option.rewardOption.type.id.includes("TEMP_STAT_STAGE_BOOSTER"), ).toBeTruthy(); } }); @@ -210,16 +209,16 @@ describe("Department Store Sale - Mystery Encounter", () => { it("should have shop with only Pokeballs", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); await runMysteryEncounterToEnd(game, 4); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(4); - for (const option of modifierSelectHandler.options) { - expect(option.modifierTypeOption.type.id).toContain("BALL"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(4); + for (const option of rewardSelectHandler.options) { + expect(option.rewardOption.type.id).toContain("BALL"); } }); diff --git a/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/test/mystery-encounter/encounters/field-trip-encounter.test.ts index 8502137cc6e..bde7ec88e5e 100644 --- a/test/mystery-encounter/encounters/field-trip-encounter.test.ts +++ b/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -9,10 +9,10 @@ import { UiMode } from "#enums/ui-mode"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import { FieldTripEncounter } from "#mystery-encounters/field-trip-encounter"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import i18next from "i18next"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -85,38 +85,38 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give no reward on incorrect option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 2 }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(0); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(0); }); - it("Should give proper rewards on correct Physical move option", async () => { + it("Should give proper allRewards on correct Physical move option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe( + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + expect(rewardSelectHandler.options[0].rewardOption.type.name).toBe( i18next.t("modifierType:TempStatStageBoosterItem.x_attack"), ); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[1].rewardOption.type.name).toBe( i18next.t("modifierType:TempStatStageBoosterItem.x_defense"), ); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[2].rewardOption.type.name).toBe( i18next.t("modifierType:TempStatStageBoosterItem.x_speed"), ); - expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[3].rewardOption.type.name).toBe( i18next.t("modifierType:ModifierType.DIRE_HIT.name"), ); - expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[4].rewardOption.type.name).toBe( i18next.t("modifierType:ModifierType.RARER_CANDY.name"), ); }); @@ -146,38 +146,38 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give no reward on incorrect option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(0); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(0); }); - it("Should give proper rewards on correct Special move option", async () => { + it("Should give proper allRewards on correct Special move option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 2 }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe( + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + expect(rewardSelectHandler.options[0].rewardOption.type.name).toBe( i18next.t("modifierType:TempStatStageBoosterItem.x_sp_atk"), ); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[1].rewardOption.type.name).toBe( i18next.t("modifierType:TempStatStageBoosterItem.x_sp_def"), ); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[2].rewardOption.type.name).toBe( i18next.t("modifierType:TempStatStageBoosterItem.x_speed"), ); - expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[3].rewardOption.type.name).toBe( i18next.t("modifierType:ModifierType.DIRE_HIT.name"), ); - expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[4].rewardOption.type.name).toBe( i18next.t("modifierType:ModifierType.RARER_CANDY.name"), ); }); @@ -207,33 +207,33 @@ describe("Field Trip - Mystery Encounter", () => { it("Should give no reward on incorrect option", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(0); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(0); }); - it("Should give proper rewards on correct Special move option", async () => { + it("Should give proper allRewards on correct Special move option", async () => { vi.spyOn(i18next, "t"); await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 3 }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe( + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + expect(rewardSelectHandler.options[0].rewardOption.type.name).toBe( i18next.t("modifierType:TempStatStageBoosterItem.x_accuracy"), ); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[1].rewardOption.type.name).toBe( i18next.t("modifierType:TempStatStageBoosterItem.x_speed"), ); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[2].rewardOption.type.name).toBe( i18next.t("modifierType:ModifierType.AddPokeballModifierType.name", { modifierCount: 5, pokeballName: i18next.t("pokeball:greatBall"), @@ -243,10 +243,10 @@ describe("Field Trip - Mystery Encounter", () => { "modifierType:ModifierType.AddPokeballModifierType.name", expect.objectContaining({ modifierCount: 5 }), ); - expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[3].rewardOption.type.name).toBe( i18next.t("modifierType:ModifierType.IV_SCANNER.name"), ); - expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe( + expect(rewardSelectHandler.options[4].rewardOption.type.name).toBe( i18next.t("modifierType:ModifierType.RARER_CANDY.name"), ); }); diff --git a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 8ec9b4cb345..c9fbe0348f1 100644 --- a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -5,6 +5,7 @@ import { Status } from "#data/status-effect"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemCategoryId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -12,14 +13,13 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; -import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import { FieryFalloutEncounter } from "#mystery-encounters/fiery-fallout-encounter"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; import { MovePhase } from "#phases/move-phase"; import { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, @@ -176,16 +176,13 @@ describe("Fiery Fallout - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); - const leadPokemonId = scene.getPlayerParty()?.[0].id; - const leadPokemonItems = scene.findModifiers( - m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, - true, - ) as PokemonHeldItemModifier[]; - const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); - expect(item).toBeDefined; + const hasAttackBooster = scene + .getPlayerParty()[0] + .heldItemManager.hasItem(HeldItemCategoryId.TYPE_ATTACK_BOOSTER); + expect(hasAttackBooster).toBe(true); }); }); @@ -265,12 +262,13 @@ describe("Fiery Fallout - Mystery Encounter", () => { it("should give attack type boosting item to lead pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); await runMysteryEncounterToEnd(game, 3); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); - const leadPokemonItems = scene.getPlayerParty()?.[0].getHeldItems() as PokemonHeldItemModifier[]; - const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); - expect(item).toBeDefined; + const hasAttackBooster = scene + .getPlayerParty()[0] + .heldItemManager.hasItem(HeldItemCategoryId.TYPE_ATTACK_BOOSTER); + expect(hasAttackBooster).toBe(true); }); it("should leave encounter without battle", async () => { diff --git a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index 8149212f00f..19d13e69a0d 100644 --- a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -12,7 +12,7 @@ import { FightOrFlightEncounter } from "#mystery-encounters/fight-or-flight-enco import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; import { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, @@ -20,7 +20,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/fightOrFlight"; @@ -122,16 +122,16 @@ describe("Fight or Flight - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(1); - expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(rewardSelectHandler.options[0].rewardOption.type.name); }); }); @@ -182,16 +182,16 @@ describe("Fight or Flight - Mystery Encounter", () => { const item = game.scene.currentBattle.mysteryEncounter!.misc; await runMysteryEncounterToEnd(game, 2); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(1); - expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(rewardSelectHandler.options[0].rewardOption.type.name); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); diff --git a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts index b66d0e95ad0..2cae72c3c8d 100644 --- a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts +++ b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -16,14 +16,14 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; import { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/funAndGames"; @@ -162,13 +162,13 @@ describe("Fun And Games! - Mystery Encounter", () => { // Turn 3 (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to(SelectRewardPhase, false); // Rewards - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); }); - it("should have no items in rewards if Wubboffet doesn't take enough damage", async () => { + it("should have no items in allRewards if Wubboffet doesn't take enough damage", async () => { scene.money = 20000; await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); @@ -181,20 +181,20 @@ describe("Fun And Games! - Mystery Encounter", () => { // Skip minigame scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to(SelectRewardPhase, false); // Rewards - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(0); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(0); }); - it("should have Wide Lens item in rewards if Wubboffet is at 15-33% HP remaining", async () => { + it("should have Wide Lens item in allRewards if Wubboffet is at 15-33% HP remaining", async () => { scene.money = 20000; game.override.moveset([MoveId.SPLASH]); await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); @@ -210,21 +210,21 @@ describe("Fun And Games! - Mystery Encounter", () => { wobbuffet.hp = Math.floor(0.2 * wobbuffet.getMaxHp()); scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to(SelectRewardPhase, false); // Rewards - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(1); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("WIDE_LENS"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(1); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toEqual("WIDE_LENS"); }); - it("should have Scope Lens item in rewards if Wubboffet is at 3-15% HP remaining", async () => { + it("should have Scope Lens item in allRewards if Wubboffet is at 3-15% HP remaining", async () => { scene.money = 20000; game.override.moveset([MoveId.SPLASH]); await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); @@ -240,21 +240,21 @@ describe("Fun And Games! - Mystery Encounter", () => { wobbuffet.hp = Math.floor(0.1 * wobbuffet.getMaxHp()); scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to(SelectRewardPhase, false); // Rewards - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(1); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("SCOPE_LENS"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(1); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toEqual("SCOPE_LENS"); }); - it("should have Multi Lens item in rewards if Wubboffet is at <3% HP remaining", async () => { + it("should have Multi Lens item in allRewards if Wubboffet is at <3% HP remaining", async () => { scene.money = 20000; game.override.moveset([MoveId.SPLASH]); await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); @@ -270,18 +270,18 @@ describe("Fun And Games! - Mystery Encounter", () => { wobbuffet.hp = 1; scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to(SelectRewardPhase, false); // Rewards - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(1); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MULTI_LENS"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(1); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toEqual("MULTI_LENS"); }); }); 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 867a33f6ab6..81a1f9fc3ec 100644 --- a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -1,22 +1,20 @@ import type { BattleScene } from "#app/battle-scene"; -import { modifierTypes } from "#data/data-lists"; import { BiomeId } from "#enums/biome-id"; -import { ModifierTier } from "#enums/modifier-tier"; +import { HeldItemId } from "#enums/held-item-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { PokemonNatureWeightModifier } from "#modifiers/modifier"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; -import { generateModifierType } from "#mystery-encounters/encounter-phase-utils"; import { GlobalTradeSystemEncounter } from "#mystery-encounters/global-trade-system-encounter"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CIVILIZATION_ENCOUNTER_BIOMES } from "#mystery-encounters/mystery-encounters"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import * as Utils from "#utils/common"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -214,29 +212,25 @@ describe("Global Trade System - Mystery Encounter", () => { }); }); - it("should decrease item stacks of chosen item and have a tiered up item in rewards", async () => { + it("should decrease item stacks of chosen item and have a tiered up item in allRewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); // Set 2 Soul Dew on party lead - scene.modifiers = []; - const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; - modifier.stackCount = 2; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOUL_DEW, 2); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(1); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier).toBe(ModifierTier.MASTER); - const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - expect(soulDewAfter?.stackCount).toBe(1); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(1); + expect(rewardSelectHandler.options[0].rewardOption.type.tier).toBe(RarityTier.MASTER); + const soulDewAfter = scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SOUL_DEW); + expect(soulDewAfter).toBe(1); }); it("should leave encounter without battle", async () => { @@ -245,12 +239,8 @@ describe("Global Trade System - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); // Set 1 Soul Dew on party lead - scene.modifiers = []; - const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; - modifier.stackCount = 1; - scene.addModifier(modifier, true, false, false, true); - await scene.updateModifiers(true); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SOUL_DEW, 1); + await scene.updateItems(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); diff --git a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index 5412f269122..4df9d75e0d6 100644 --- a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -1,11 +1,11 @@ import type { BattleScene } from "#app/battle-scene"; import { BiomeId } from "#enums/biome-id"; -import { ModifierTier } from "#enums/modifier-tier"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { MysteriousChallengersEncounter } from "#mystery-encounters/mysterious-challengers-encounter"; @@ -13,7 +13,7 @@ import { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, @@ -22,7 +22,7 @@ import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; import { TrainerConfig } from "#trainers/trainer-config"; import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/mysteriousChallengers"; @@ -157,22 +157,22 @@ describe("Mysterious Challengers - Mystery Encounter", () => { expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); }); - it("should have normal trainer rewards after battle", async () => { + it("should have normal trainer allRewards after battle", async () => { await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toContain("TM_COMMON"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toContain("TM_GREAT"); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toContain("MEMORY_MUSHROOM"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toContain("TM_COMMON"); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toContain("TM_GREAT"); + expect(rewardSelectHandler.options[2].rewardOption.type.id).toContain("MEMORY_MUSHROOM"); }); }); @@ -201,35 +201,35 @@ describe("Mysterious Challengers - Mystery Encounter", () => { expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); }); - it("should have hard trainer rewards after battle", async () => { + it("should have hard trainer allRewards after battle", async () => { await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(4); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(4); expect( - modifierSelectHandler.options[0].modifierTypeOption.type.tier - - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ULTRA); + rewardSelectHandler.options[0].rewardOption.type.tier - + rewardSelectHandler.options[0].rewardOption.upgradeCount, + ).toBe(RarityTier.ULTRA); expect( - modifierSelectHandler.options[1].modifierTypeOption.type.tier - - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ULTRA); + rewardSelectHandler.options[1].rewardOption.type.tier - + rewardSelectHandler.options[1].rewardOption.upgradeCount, + ).toBe(RarityTier.ULTRA); expect( - modifierSelectHandler.options[2].modifierTypeOption.type.tier - - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.GREAT); + rewardSelectHandler.options[2].rewardOption.type.tier - + rewardSelectHandler.options[2].rewardOption.upgradeCount, + ).toBe(RarityTier.GREAT); expect( - modifierSelectHandler.options[3].modifierTypeOption.type.tier - - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.GREAT); + rewardSelectHandler.options[3].rewardOption.type.tier - + rewardSelectHandler.options[3].rewardOption.upgradeCount, + ).toBe(RarityTier.GREAT); }); }); @@ -258,35 +258,35 @@ describe("Mysterious Challengers - Mystery Encounter", () => { expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); }); - it("should have brutal trainer rewards after battle", async () => { + it("should have brutal trainer allRewards after battle", async () => { await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); await runMysteryEncounterToEnd(game, 3, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(4); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(4); expect( - modifierSelectHandler.options[0].modifierTypeOption.type.tier - - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ROGUE); + rewardSelectHandler.options[0].rewardOption.type.tier - + rewardSelectHandler.options[0].rewardOption.upgradeCount, + ).toBe(RarityTier.ROGUE); expect( - modifierSelectHandler.options[1].modifierTypeOption.type.tier - - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ROGUE); + rewardSelectHandler.options[1].rewardOption.type.tier - + rewardSelectHandler.options[1].rewardOption.upgradeCount, + ).toBe(RarityTier.ROGUE); expect( - modifierSelectHandler.options[2].modifierTypeOption.type.tier - - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.ULTRA); + rewardSelectHandler.options[2].rewardOption.type.tier - + rewardSelectHandler.options[2].rewardOption.upgradeCount, + ).toBe(RarityTier.ULTRA); expect( - modifierSelectHandler.options[3].modifierTypeOption.type.tier - - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toBe(ModifierTier.GREAT); + rewardSelectHandler.options[3].rewardOption.type.tier - + rewardSelectHandler.options[3].rewardOption.upgradeCount, + ).toBe(RarityTier.GREAT); }); }); }); diff --git a/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts index ff4f73cfbde..1b1990c2f93 100644 --- a/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts +++ b/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -10,7 +10,7 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { TeleportingHijinksEncounter } from "#mystery-encounters/teleporting-hijinks-encounter"; import { CommandPhase } from "#phases/command-phase"; import { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, @@ -18,7 +18,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import i18next from "i18next"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -295,26 +295,26 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { expect(enemyField[0].isBoss()).toBe(true); }); - it("should have Magnet and Metal Coat in rewards after battle", async () => { + it("should have Magnet and Metal Coat in allRewards after battle", async () => { await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); await runMysteryEncounterToEnd(game, 3, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; expect( - modifierSelectHandler.options.some( - opt => opt.modifierTypeOption.type.name === i18next.t("modifierType:AttackTypeBoosterItem.metal_coat"), + rewardSelectHandler.options.some( + opt => opt.rewardOption.type.name === i18next.t("modifierType:AttackTypeBoosterItem.metal_coat"), ), ).toBe(true); expect( - modifierSelectHandler.options.some( - opt => opt.modifierTypeOption.type.name === i18next.t("modifierType:AttackTypeBoosterItem.magnet"), + rewardSelectHandler.options.some( + opt => opt.rewardOption.type.name === i18next.t("modifierType:AttackTypeBoosterItem.magnet"), ), ).toBe(true); }); diff --git a/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts b/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts index 4556f7a7f45..ae7ba6d9544 100644 --- a/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts @@ -14,7 +14,7 @@ import { HUMAN_TRANSITABLE_BIOMES } from "#mystery-encounters/mystery-encounters import { TheExpertPokemonBreederEncounter } from "#mystery-encounters/the-expert-pokemon-breeder-encounter"; import { CommandPhase } from "#phases/command-phase"; import { PostMysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, @@ -176,8 +176,8 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); const eggsAfter = scene.gameData.eggs; const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon1CommonEggs; @@ -261,8 +261,8 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); const eggsAfter = scene.gameData.eggs; const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon2CommonEggs; @@ -343,8 +343,8 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); const eggsAfter = scene.gameData.eggs; const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon3CommonEggs; diff --git a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index a314a14485f..872b2f9c076 100644 --- a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -3,8 +3,9 @@ import * as BattleAnims from "#data/battle-anims"; import { CustomPokemonData } from "#data/pokemon-data"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemEffect } from "#enums/held-item-effect"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -12,21 +13,21 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Nature } from "#enums/nature"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { BerryModifier, PokemonBaseStatTotalModifier } from "#modifiers/modifier"; +import { applyHeldItems } from "#items/all-held-items"; import { PokemonMove } from "#moves/pokemon-move"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { TheStrongStuffEncounter } from "#mystery-encounters/the-strong-stuff-encounter"; import { CommandPhase } from "#phases/command-phase"; import { MovePhase } from "#phases/move-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -113,7 +114,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), nature: Nature.HARDY, moveSet: [MoveId.INFESTATION, MoveId.SALT_CURE, MoveId.GASTRO_ACID, MoveId.HEAL_ORDER], - modifierConfigs: expect.any(Array), + heldItemConfig: expect.any(Array), tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: expect.any(Function), }, @@ -149,7 +150,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { const bstsAfter = scene.getPlayerParty().map(p => { const baseStats = p.getSpeciesForm().baseStats.slice(0); - scene.applyModifiers(PokemonBaseStatTotalModifier, true, p, baseStats); + applyHeldItems(HeldItemEffect.BASE_STAT_TOTAL, { pokemon: p, baseStats: baseStats }); return baseStats.reduce((a, b) => a + b); }); @@ -198,19 +199,11 @@ describe("The Strong Stuff - Mystery Encounter", () => { expect(enemyField[0].summonData.statStages).toEqual([0, 1, 0, 1, 0, 0, 0]); const shuckleItems = enemyField[0].getHeldItems(); expect(shuckleItems.length).toBe(5); - expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.SITRUS)?.stackCount).toBe( - 1, - ); - expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.ENIGMA)?.stackCount).toBe( - 1, - ); - expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.GANLON)?.stackCount).toBe( - 1, - ); - expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.APICOT)?.stackCount).toBe( - 1, - ); - expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.LUM)?.stackCount).toBe(2); + expect(enemyField[0].heldItemManager.getStack(HeldItemId.SITRUS_BERRY)).toBe(1); + expect(enemyField[0].heldItemManager.getStack(HeldItemId.ENIGMA_BERRY)).toBe(1); + expect(enemyField[0].heldItemManager.getStack(HeldItemId.GANLON_BERRY)).toBe(1); + expect(enemyField[0].heldItemManager.getStack(HeldItemId.APICOT_BERRY)).toBe(1); + expect(enemyField[0].heldItemManager.getStack(HeldItemId.LUM_BERRY)).toBe(2); expect(enemyField[0].moveset).toEqual([ new PokemonMove(MoveId.INFESTATION), new PokemonMove(MoveId.SALT_CURE), @@ -225,20 +218,20 @@ describe("The Strong Stuff - Mystery Encounter", () => { expect(movePhases.filter(p => (p as MovePhase).move.moveId === MoveId.STEALTH_ROCK).length).toBe(1); }); - it("should have Soul Dew in rewards", async () => { + it("should have Soul Dew in allRewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("SOUL_DEW"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toEqual("SOUL_DEW"); }); }); }); diff --git a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index ae2f9fd79ff..8be8061fdfa 100644 --- a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -18,12 +18,12 @@ import { TheWinstrateChallengeEncounter } from "#mystery-encounters/the-winstrat import { CommandPhase } from "#phases/command-phase"; import { MysteryEncounterRewardsPhase } from "#phases/mystery-encounter-phases"; import { PartyHealPhase } from "#phases/party-heal-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { VictoryPhase } from "#phases/victory-phase"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -293,18 +293,18 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(0); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); - // Should have Macho Brace in the rewards + // Should have Macho Brace in the allRewards await skipBattleToNextBattle(game, true); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(1); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MYSTERY_ENCOUNTER_MACHO_BRACE"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(1); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toBe("MYSTERY_ENCOUNTER_MACHO_BRACE"); }); }); @@ -335,18 +335,18 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { expect(partyHealPhases.length).toBe(1); }); - it("should have a Rarer Candy in the rewards", async () => { + it("should have a Rarer Candy in the allRewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); await runMysteryEncounterToEnd(game, 2); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(1); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("RARER_CANDY"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(1); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toBe("RARER_CANDY"); }); }); }); 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 133fbfb10ba..1e4d1c3deba 100644 --- a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -1,36 +1,32 @@ import type { BattleScene } from "#app/battle-scene"; import * as BattleAnims from "#data/battle-anims"; -import { modifierTypes } from "#data/data-lists"; import { BiomeId } from "#enums/biome-id"; -import { ModifierTier } from "#enums/modifier-tier"; +import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { UiMode } from "#enums/ui-mode"; -import { HealShopCostModifier, HitHealModifier, TurnHealModifier } from "#modifiers/modifier"; -import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; +import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; -import { - type EnemyPartyConfig, - type EnemyPokemonConfig, - generateModifierType, -} from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { TrashToTreasureEncounter } from "#mystery-encounters/trash-to-treasure-encounter"; import { CommandPhase } from "#phases/command-phase"; import { MovePhase } from "#phases/move-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import * as Utils from "#utils/common"; +import { randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -101,41 +97,13 @@ describe("Trash to Treasure - Mystery Encounter", () => { 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: Utils.randSeedInt(2, 0), - }, - { - modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType, - stackCount: Utils.randSeedInt(2, 1), - }, - { - modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType, - stackCount: Utils.randSeedInt(3, 1), - }, - { - modifier: generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType, - stackCount: Utils.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 = { @@ -172,20 +140,14 @@ describe("Trash to Treasure - Mystery Encounter", () => { it("should give 2 Leftovers, 1 Shell Bell, and Black Sludge", async () => { await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); await runMysteryEncounterToEnd(game, 1); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); - const leftovers = scene.findModifier(m => m instanceof TurnHealModifier) as TurnHealModifier; - expect(leftovers).toBeDefined(); - expect(leftovers?.stackCount).toBe(2); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.LEFTOVERS)).toBe(2); - const shellBell = scene.findModifier(m => m instanceof HitHealModifier) as HitHealModifier; - expect(shellBell).toBeDefined(); - expect(shellBell?.stackCount).toBe(1); + expect(scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.SHELL_BELL)).toBe(1); - const blackSludge = scene.findModifier(m => m instanceof HealShopCostModifier) as HealShopCostModifier; - expect(blackSludge).toBeDefined(); - expect(blackSludge?.stackCount).toBe(1); + expect(scene.trainerItems.getStack(TrainerItemId.BLACK_SLUDGE)).toBe(1); }); it("should leave encounter without battle", async () => { @@ -238,35 +200,35 @@ describe("Trash to Treasure - Mystery Encounter", () => { expect(movePhases.filter(p => (p as MovePhase).move.moveId === MoveId.STOCKPILE).length).toBe(1); }); - it("should have 2 Rogue, 1 Ultra, 1 Great in rewards", async () => { + it("should have 2 Rogue, 1 Ultra, 1 Great in allRewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(4); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(4); expect( - modifierSelectHandler.options[0].modifierTypeOption.type.tier - - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + rewardSelectHandler.options[0].rewardOption.type.tier - + rewardSelectHandler.options[0].rewardOption.upgradeCount, + ).toEqual(RarityTier.ROGUE); expect( - modifierSelectHandler.options[1].modifierTypeOption.type.tier - - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + rewardSelectHandler.options[1].rewardOption.type.tier - + rewardSelectHandler.options[1].rewardOption.upgradeCount, + ).toEqual(RarityTier.ROGUE); expect( - modifierSelectHandler.options[2].modifierTypeOption.type.tier - - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ULTRA); + rewardSelectHandler.options[2].rewardOption.type.tier - + rewardSelectHandler.options[2].rewardOption.upgradeCount, + ).toEqual(RarityTier.ULTRA); expect( - modifierSelectHandler.options[3].modifierTypeOption.type.tier - - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.GREAT); + rewardSelectHandler.options[3].rewardOption.type.tier - + rewardSelectHandler.options[3].rewardOption.upgradeCount, + ).toEqual(RarityTier.GREAT); }); }); }); diff --git a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index 24d7960049e..ea0c00dc5ad 100644 --- a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -1,19 +1,16 @@ import type { BattleScene } from "#app/battle-scene"; import { speciesEggMoves } from "#balance/egg-moves"; -import { modifierTypes } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; -import { BerryType } from "#enums/berry-type"; import { BiomeId } from "#enums/biome-id"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import type { BerryModifier } from "#modifiers/modifier"; import { PokemonMove } from "#moves/pokemon-move"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; -import { generateModifierType } from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { UncommonBreedEncounter } from "#mystery-encounters/uncommon-breed-encounter"; import { CommandPhase } from "#phases/command-phase"; @@ -184,11 +181,6 @@ describe("Uncommon Breed - Mystery Encounter", () => { // TODO: there is some severe test flakiness occurring for this file, needs to be looked at/addressed in separate issue it.skip("should NOT be selectable if the player doesn't have enough berries", async () => { await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); - // Clear out any pesky mods that slipped through test spin-up - scene.modifiers.forEach(mod => { - scene.removeModifier(mod); - }); - await scene.updateModifiers(true); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.phaseManager.getCurrentPhase(); @@ -213,15 +205,9 @@ describe("Uncommon Breed - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); // Berries on party lead - const sitrus = generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS])!; - const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; - sitrusMod.stackCount = 2; - scene.addModifier(sitrusMod, true, false, false, true); - const ganlon = generateModifierType(modifierTypes.BERRY, [BerryType.GANLON])!; - const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier; - ganlonMod.stackCount = 3; - scene.addModifier(ganlonMod, true, false, false, true); - await scene.updateModifiers(true); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.SITRUS_BERRY, 2); + scene.getPlayerParty()[0].heldItemManager.add(HeldItemId.GANLON_BERRY, 3); + scene.updateItems(true); await runMysteryEncounterToEnd(game, 2); diff --git a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index e9fcc9797d1..c32779e26aa 100644 --- a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -1,9 +1,9 @@ import type { BattleScene } from "#app/battle-scene"; import { BiomeId } from "#enums/biome-id"; -import { ModifierTier } from "#enums/modifier-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { RarityTier } from "#enums/reward-tier"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; @@ -11,14 +11,14 @@ import * as EncounterTransformationSequence from "#mystery-encounters/encounter- import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { WeirdDreamEncounter } from "#mystery-encounters/weird-dream-encounter"; import { CommandPhase } from "#phases/command-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase, } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/weirdDream"; @@ -116,8 +116,8 @@ describe("Weird Dream - Mystery Encounter", () => { const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal()); await runMysteryEncounterToEnd(game, 1); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); const pokemonAfter = scene.getPlayerParty(); const bstsAfter = pokemonAfter.map(pokemon => pokemon.getSpeciesForm().getBaseStatTotal()); @@ -136,23 +136,23 @@ describe("Weird Dream - Mystery Encounter", () => { expect(plus40To50.length).toBe(1); }); - it("should have 1 Memory Mushroom, 5 Rogue Balls, and 3 Mints in rewards", async () => { + it("should have 1 Memory Mushroom, 5 Rogue Balls, and 3 Mints in allRewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await runMysteryEncounterToEnd(game, 1); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL"); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT"); - expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT"); - expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT"); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toEqual("ROGUE_BALL"); + expect(rewardSelectHandler.options[2].rewardOption.type.id).toEqual("MINT"); + expect(rewardSelectHandler.options[3].rewardOption.type.id).toEqual("MINT"); + expect(rewardSelectHandler.options[3].rewardOption.type.id).toEqual("MINT"); }); it("should leave encounter without battle", async () => { @@ -191,43 +191,43 @@ describe("Weird Dream - Mystery Encounter", () => { expect(scene.getEnemyParty().length).toBe(scene.getPlayerParty().length); }); - it("should have 2 Rogue/2 Ultra/2 Great items in rewards", async () => { + it("should have 2 Rogue/2 Ultra/2 Great items in allRewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to(SelectRewardPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectRewardPhase.name); + await game.phaseInterceptor.run(SelectRewardPhase); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(6); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(6); expect( - modifierSelectHandler.options[0].modifierTypeOption.type.tier - - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + rewardSelectHandler.options[0].rewardOption.type.tier - + rewardSelectHandler.options[0].rewardOption.upgradeCount, + ).toEqual(RarityTier.ROGUE); expect( - modifierSelectHandler.options[1].modifierTypeOption.type.tier - - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); + rewardSelectHandler.options[1].rewardOption.type.tier - + rewardSelectHandler.options[1].rewardOption.upgradeCount, + ).toEqual(RarityTier.ROGUE); expect( - modifierSelectHandler.options[2].modifierTypeOption.type.tier - - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ULTRA); + rewardSelectHandler.options[2].rewardOption.type.tier - + rewardSelectHandler.options[2].rewardOption.upgradeCount, + ).toEqual(RarityTier.ULTRA); expect( - modifierSelectHandler.options[3].modifierTypeOption.type.tier - - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ULTRA); + rewardSelectHandler.options[3].rewardOption.type.tier - + rewardSelectHandler.options[3].rewardOption.upgradeCount, + ).toEqual(RarityTier.ULTRA); expect( - modifierSelectHandler.options[4].modifierTypeOption.type.tier - - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.GREAT); + rewardSelectHandler.options[4].rewardOption.type.tier - + rewardSelectHandler.options[4].rewardOption.upgradeCount, + ).toEqual(RarityTier.GREAT); expect( - modifierSelectHandler.options[5].modifierTypeOption.type.tier - - modifierSelectHandler.options[5].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.GREAT); + rewardSelectHandler.options[5].rewardOption.type.tier - + rewardSelectHandler.options[5].rewardOption.upgradeCount, + ).toEqual(RarityTier.GREAT); }); }); diff --git a/test/phases/form-change-phase.test.ts b/test/phases/form-change-phase.test.ts index 3caf824b252..df0bd74f8ef 100644 --- a/test/phases/form-change-phase.test.ts +++ b/test/phases/form-change-phase.test.ts @@ -1,60 +1,59 @@ -import { modifierTypes } from "#data/data-lists"; -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { PokemonType } from "#enums/pokemon-type"; -import { SpeciesId } from "#enums/species-id"; -import { generateModifierType } from "#mystery-encounters/encounter-phase-utils"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { RewardKeys } from "#items/reward"; +import { itemPoolChecks } from "#items/reward"; +import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; +import { expect } from "vitest"; -describe("Form Change Phase", () => { - let phaserGame: Phaser.Game; - let game: GameManager; +export class ModifierHelper extends GameManagerHelper { + /** + * Adds a Modifier to the list of modifiers to check for. + * + * Note that all modifiers are updated during the start of `SelectRewardPhase`. + * @param modifier The Modifier to add. + * @returns `this` + */ + addCheck(modifier: RewardKeys): this { + itemPoolChecks.set(modifier, undefined); + return this; + } - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); + /** + * `get`s a value from the `itemPoolChecks` map. + * + * If the item is in the Modifier Pool, and the player can get it, will return `true`. + * + * If the item is *not* in the Modifier Pool, will return `false`. + * + * If a `SelectRewardPhase` has not occurred, and we do not know if the item is in the Modifier Pool or not, will return `undefined`. + * @param modifier + * @returns + */ + getCheck(modifier: RewardKeys): boolean | undefined { + return itemPoolChecks.get(modifier); + } - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); + /** + * `expect`s a Modifier `toBeTruthy` (in the Modifier Pool) or `Falsy` (unobtainable on this floor). Use during a test. + * + * Note that if a `SelectRewardPhase` has not been run yet, these values will be `undefined`, and the check will fail. + * @param modifier The modifier to check. + * @param expectToBePreset Whether the Modifier should be in the Modifier Pool. Set to `false` to expect it to be absent instead. + * @returns `this` + */ + testCheck(modifier: RewardKeys, expectToBePreset: boolean): this { + if (expectToBePreset) { + expect(itemPoolChecks.get(modifier)).toBeTruthy(); + } + expect(itemPoolChecks.get(modifier)).toBeFalsy(); + return this; + } - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) - .battleStyle("single") - .criticalHits(false) - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); + /** Removes all modifier checks. @returns `this` */ + clearChecks() { + itemPoolChecks.clear(); + return this; + } - it("Zacian should successfully change into Crowned form", async () => { - await game.classicMode.startBattle([SpeciesId.ZACIAN]); - - // Before the form change: Should be Hero form - const zacian = game.scene.getPlayerParty()[0]; - expect(zacian.getFormKey()).toBe("hero-of-many-battles"); - expect(zacian.getTypes()).toStrictEqual([PokemonType.FAIRY]); - expect(zacian.calculateBaseStats()).toStrictEqual([92, 120, 115, 80, 115, 138]); - - // Give Zacian a Rusted Sword - const rustedSwordType = generateModifierType(modifierTypes.RARE_FORM_CHANGE_ITEM)!; - const rustedSword = rustedSwordType.newModifier(zacian); - await game.scene.addModifier(rustedSword); - - game.move.select(MoveId.SPLASH); - await game.toNextTurn(); - - // After the form change: Should be Crowned form - expect(game.phaseInterceptor.log.includes("FormChangePhase")).toBe(true); - expect(zacian.getFormKey()).toBe("crowned"); - expect(zacian.getTypes()).toStrictEqual([PokemonType.FAIRY, PokemonType.STEEL]); - expect(zacian.calculateBaseStats()).toStrictEqual([92, 150, 115, 80, 115, 148]); - }); -}); + private log(...params: any[]) { + console.log("Modifiers:", ...params); + } +} \ No newline at end of file diff --git a/test/phases/game-over-phase.test.ts b/test/phases/game-over-phase.test.ts index 201eebc5264..2baf1939b29 100644 --- a/test/phases/game-over-phase.test.ts +++ b/test/phases/game-over-phase.test.ts @@ -36,7 +36,7 @@ describe("Game Over Phase", () => { .startingLevel(10000); }); - it("winning a run should give rewards", async () => { + it("winning a run should give allRewards", async () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); vi.spyOn(game.scene, "validateAchv"); @@ -53,13 +53,13 @@ describe("Game Over Phase", () => { // so the best we can do is to check that their reward phases occurred. expect(game.phaseInterceptor.log.includes("GameOverPhase")).toBe(true); expect(game.phaseInterceptor.log.includes("UnlockPhase")).toBe(true); - expect(game.phaseInterceptor.log.includes("RibbonModifierRewardPhase")).toBe(true); + expect(game.phaseInterceptor.log.includes("RibbonRewardPhase")).toBe(true); expect(game.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]).toBe(true); expect(game.scene.validateAchv).toHaveBeenCalledWith(achvs.CLASSIC_VICTORY); expect(game.scene.gameData.achvUnlocks[achvs.CLASSIC_VICTORY.id]).toBeTruthy(); }); - it("losing a run should not give rewards", async () => { + it("losing a run should not give allRewards", async () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); vi.spyOn(game.scene, "validateAchv"); @@ -68,8 +68,8 @@ describe("Game Over Phase", () => { expect(game.phaseInterceptor.log.includes("GameOverPhase")).toBe(true); expect(game.phaseInterceptor.log.includes("UnlockPhase")).toBe(false); - expect(game.phaseInterceptor.log.includes("RibbonModifierRewardPhase")).toBe(false); - expect(game.phaseInterceptor.log.includes("GameOverModifierRewardPhase")).toBe(false); + expect(game.phaseInterceptor.log.includes("RibbonRewardPhase")).toBe(false); + expect(game.phaseInterceptor.log.includes("GameOverRewardPhase")).toBe(false); expect(game.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]).toBe(false); expect(game.scene.validateAchv).not.toHaveBeenCalledWith(achvs.CLASSIC_VICTORY); expect(game.scene.gameData.achvUnlocks[achvs.CLASSIC_VICTORY.id]).toBeFalsy(); diff --git a/test/phases/select-modifier-phase.test.ts b/test/phases/select-modifier-phase.test.ts deleted file mode 100644 index ae4cebb1866..00000000000 --- a/test/phases/select-modifier-phase.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { BattleScene } from "#app/battle-scene"; -import { modifierTypes } from "#data/data-lists"; -import { AbilityId } from "#enums/ability-id"; -import { Button } from "#enums/buttons"; -import { ModifierTier } from "#enums/modifier-tier"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { UiMode } from "#enums/ui-mode"; -import { PlayerPokemon } from "#field/pokemon"; -import type { CustomModifierSettings } from "#modifiers/modifier-type"; -import { ModifierTypeOption } from "#modifiers/modifier-type"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; -import { GameManager } from "#test/test-utils/game-manager"; -import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; -import { shiftCharCodes } from "#utils/common"; -import { getPokemonSpecies } from "#utils/pokemon-utils"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -describe("SelectModifierPhase", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - let scene: BattleScene; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - scene = game.scene; - - game.override - .moveset([MoveId.FISSURE, MoveId.SPLASH]) - .ability(AbilityId.NO_GUARD) - .startingLevel(200) - .enemySpecies(SpeciesId.MAGIKARP); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - it("should start a select modifier phase", async () => { - initSceneWithoutEncounterPhase(scene, [SpeciesId.ABRA, SpeciesId.VOLCARONA]); - const selectModifierPhase = new SelectModifierPhase(); - scene.phaseManager.unshiftPhase(selectModifierPhase); - await game.phaseInterceptor.to(SelectModifierPhase); - - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - }); - - it("should generate random modifiers", async () => { - await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); - game.move.select(MoveId.FISSURE); - await game.phaseInterceptor.to("SelectModifierPhase"); - - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); - }); - - it("should modify reroll cost", async () => { - initSceneWithoutEncounterPhase(scene, [SpeciesId.ABRA, SpeciesId.VOLCARONA]); - const options = [ - new ModifierTypeOption(modifierTypes.POTION(), 0, 100), - new ModifierTypeOption(modifierTypes.ETHER(), 0, 400), - new ModifierTypeOption(modifierTypes.REVIVE(), 0, 1000), - ]; - - const selectModifierPhase1 = new SelectModifierPhase(0, undefined, { - guaranteedModifierTypeOptions: options, - }); - const selectModifierPhase2 = new SelectModifierPhase(0, undefined, { - guaranteedModifierTypeOptions: options, - rerollMultiplier: 2, - }); - - const cost1 = selectModifierPhase1.getRerollCost(false); - const cost2 = selectModifierPhase2.getRerollCost(false); - expect(cost2).toEqual(cost1 * 2); - }); - - it.todo("should generate random modifiers from reroll", async () => { - await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); - scene.money = 1000000; - scene.shopCursorTarget = 0; - - game.move.select(MoveId.FISSURE); - await game.phaseInterceptor.to("SelectModifierPhase"); - - // TODO: nagivate the ui to reroll somehow - //const smphase = scene.phaseManager.getCurrentPhase() as SelectModifierPhase; - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); - - modifierSelectHandler.processInput(Button.ACTION); - - expect(scene.money).toBe(1000000 - 250); - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - expect(modifierSelectHandler.options.length).toEqual(3); - }); - - it.todo("should generate random modifiers of same tier for reroll with reroll lock", async () => { - game.override.startingModifier([{ name: "LOCK_CAPSULE" }]); - await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); - scene.money = 1000000; - // Just use fully random seed for this test - vi.spyOn(scene, "resetSeed").mockImplementation(() => { - scene.waveSeed = shiftCharCodes(scene.seed, 5); - Phaser.Math.RND.sow([scene.waveSeed]); - console.log("Wave Seed:", scene.waveSeed, 5); - scene.rngCounter = 0; - }); - - game.move.select(MoveId.FISSURE); - await game.phaseInterceptor.to("SelectModifierPhase"); - - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(3); - const firstRollTiers: ModifierTier[] = modifierSelectHandler.options.map(o => o.modifierTypeOption.type.tier); - - // TODO: nagivate ui to reroll with lock capsule enabled - - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - expect(modifierSelectHandler.options.length).toEqual(3); - // Reroll with lock can still upgrade - expect( - modifierSelectHandler.options[0].modifierTypeOption.type.tier - - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toEqual(firstRollTiers[0]); - expect( - modifierSelectHandler.options[1].modifierTypeOption.type.tier - - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toEqual(firstRollTiers[1]); - expect( - modifierSelectHandler.options[2].modifierTypeOption.type.tier - - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toEqual(firstRollTiers[2]); - }); - - it("should generate custom modifiers", async () => { - await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); - scene.money = 1000000; - const customModifiers: CustomModifierSettings = { - guaranteedModifierTypeFuncs: [ - modifierTypes.MEMORY_MUSHROOM, - modifierTypes.TM_ULTRA, - modifierTypes.LEFTOVERS, - modifierTypes.AMULET_COIN, - modifierTypes.GOLDEN_PUNCH, - ], - }; - const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); - scene.phaseManager.unshiftPhase(selectModifierPhase); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to("SelectModifierPhase"); - - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); - expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("TM_ULTRA"); - expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("LEFTOVERS"); - expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("AMULET_COIN"); - expect(modifierSelectHandler.options[4].modifierTypeOption.type.id).toEqual("GOLDEN_PUNCH"); - }); - - it("should generate custom modifier tiers that can upgrade from luck", async () => { - await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); - scene.money = 1000000; - const customModifiers: CustomModifierSettings = { - guaranteedModifierTiers: [ - ModifierTier.COMMON, - ModifierTier.GREAT, - ModifierTier.ULTRA, - ModifierTier.ROGUE, - ModifierTier.MASTER, - ], - }; - const pokemon = new PlayerPokemon(getPokemonSpecies(SpeciesId.BULBASAUR), 10, undefined, 0, undefined, true, 2); - - // Fill party with max shinies - while (scene.getPlayerParty().length > 0) { - scene.getPlayerParty().pop(); - } - scene.getPlayerParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon); - - const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); - scene.phaseManager.unshiftPhase(selectModifierPhase); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to("SelectModifierPhase"); - - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(5); - expect( - modifierSelectHandler.options[0].modifierTypeOption.type.tier - - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.COMMON); - expect( - modifierSelectHandler.options[1].modifierTypeOption.type.tier - - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.GREAT); - expect( - modifierSelectHandler.options[2].modifierTypeOption.type.tier - - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ULTRA); - expect( - modifierSelectHandler.options[3].modifierTypeOption.type.tier - - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.ROGUE); - expect( - modifierSelectHandler.options[4].modifierTypeOption.type.tier - - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount, - ).toEqual(ModifierTier.MASTER); - }); - - it("should generate custom modifiers and modifier tiers together", async () => { - await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); - scene.money = 1000000; - const customModifiers: CustomModifierSettings = { - guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.TM_COMMON], - guaranteedModifierTiers: [ModifierTier.MASTER, ModifierTier.MASTER], - }; - const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); - scene.phaseManager.unshiftPhase(selectModifierPhase); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.run(SelectModifierPhase); - - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) as ModifierSelectUiHandler; - 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); - }); - - it("should fill remaining modifiers if fillRemaining is true with custom modifiers", async () => { - await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); - scene.money = 1000000; - const customModifiers: CustomModifierSettings = { - guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM], - guaranteedModifierTiers: [ModifierTier.MASTER], - fillRemaining: true, - }; - const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); - scene.phaseManager.unshiftPhase(selectModifierPhase); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.run(SelectModifierPhase); - - expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); - const modifierSelectHandler = scene.ui.handlers.find( - h => h instanceof ModifierSelectUiHandler, - ) 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); - }); -}); diff --git a/test/phases/select-reward-phase.test.ts b/test/phases/select-reward-phase.test.ts new file mode 100644 index 00000000000..9bd17f434db --- /dev/null +++ b/test/phases/select-reward-phase.test.ts @@ -0,0 +1,281 @@ +import type { BattleScene } from "#app/battle-scene"; +import { allRewards } from "#data/data-lists"; +import { AbilityId } from "#enums/ability-id"; +import { Button } from "#enums/buttons"; +import { HeldItemId } from "#enums/held-item-id"; +import { MoveId } from "#enums/move-id"; +import { RewardId } from "#enums/reward-id"; +import { RarityTier } from "#enums/reward-tier"; +import { SpeciesId } from "#enums/species-id"; +import { TrainerItemId } from "#enums/trainer-item-id"; +import { UiMode } from "#enums/ui-mode"; +import { PlayerPokemon } from "#field/pokemon"; +import type { HeldItemReward, TrainerItemReward } from "#items/reward"; +import { RewardOption } from "#items/reward"; +import type { CustomRewardSettings } from "#items/reward-pool-utils"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; +import { GameManager } from "#test/test-utils/game-manager"; +import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; +import { shiftCharCodes } from "#utils/common"; +import { getPokemonSpecies } from "#utils/pokemon-utils"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("SelectRewardPhase", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + scene = game.scene; + + game.override + .moveset([MoveId.FISSURE, MoveId.SPLASH]) + .ability(AbilityId.NO_GUARD) + .startingLevel(200) + .enemySpecies(SpeciesId.MAGIKARP); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + it("should start a select modifier phase", async () => { + initSceneWithoutEncounterPhase(scene, [SpeciesId.ABRA, SpeciesId.VOLCARONA]); + const selectRewardPhase = new SelectRewardPhase(); + scene.phaseManager.unshiftPhase(selectRewardPhase); + await game.phaseInterceptor.to(SelectRewardPhase); + + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + }); + + it("should generate random modifiers", async () => { + await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); + game.move.select(MoveId.FISSURE); + await game.phaseInterceptor.to("SelectRewardPhase"); + + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + }); + + it("should modify reroll cost", async () => { + initSceneWithoutEncounterPhase(scene, [SpeciesId.ABRA, SpeciesId.VOLCARONA]); + const options = [ + new RewardOption(allRewards.POTION(), 0, RarityTier.COMMON, 100), + new RewardOption(allRewards.ETHER(), 0, RarityTier.COMMON, 400), + new RewardOption(allRewards.REVIVE(), 0, RarityTier.COMMON, 1000), + ]; + + const selectRewardPhase1 = new SelectRewardPhase(0, undefined, { + guaranteedRewardOptions: options, + }); + const selectRewardPhase2 = new SelectRewardPhase(0, undefined, { + guaranteedRewardOptions: options, + rerollMultiplier: 2, + }); + + const cost1 = selectRewardPhase1.getRerollCost(false); + const cost2 = selectRewardPhase2.getRerollCost(false); + expect(cost2).toEqual(cost1 * 2); + }); + + it.todo("should generate random modifiers from reroll", async () => { + await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); + scene.money = 1000000; + scene.shopCursorTarget = 0; + + game.move.select(MoveId.FISSURE); + await game.phaseInterceptor.to("SelectRewardPhase"); + + // TODO: nagivate the ui to reroll somehow + //const smphase = scene.phaseManager.getCurrentPhase() as SelectRewardPhase; + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + + rewardSelectHandler.processInput(Button.ACTION); + + expect(scene.money).toBe(1000000 - 250); + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + expect(rewardSelectHandler.options.length).toEqual(3); + }); + + it.todo("should generate random modifiers of same tier for reroll with reroll lock", async () => { + game.override.startingTrainerItems([{ entry: TrainerItemId.LOCK_CAPSULE }]); + await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); + scene.money = 1000000; + // Just use fully random seed for this test + vi.spyOn(scene, "resetSeed").mockImplementation(() => { + scene.waveSeed = shiftCharCodes(scene.seed, 5); + Phaser.Math.RND.sow([scene.waveSeed]); + console.log("Wave Seed:", scene.waveSeed, 5); + scene.rngCounter = 0; + }); + + game.move.select(MoveId.FISSURE); + await game.phaseInterceptor.to("SelectRewardPhase"); + + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + const firstRollTiers: RarityTier[] = rewardSelectHandler.options.map(o => o.rewardOption.tier); + + // TODO: nagivate ui to reroll with lock capsule enabled + + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + expect(rewardSelectHandler.options.length).toEqual(3); + // Reroll with lock can still upgrade + expect( + rewardSelectHandler.options[0].rewardOption.tier - rewardSelectHandler.options[0].rewardOption.upgradeCount, + ).toEqual(firstRollTiers[0]); + expect( + rewardSelectHandler.options[1].rewardOption.tier - rewardSelectHandler.options[1].rewardOption.upgradeCount, + ).toEqual(firstRollTiers[1]); + expect( + rewardSelectHandler.options[2].rewardOption.tier - rewardSelectHandler.options[2].rewardOption.upgradeCount, + ).toEqual(firstRollTiers[2]); + }); + + it("should generate custom modifiers", async () => { + await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); + scene.money = 1000000; + const customRewards: CustomRewardSettings = { + guaranteedRewardSpecs: [ + allRewards.MEMORY_MUSHROOM, + allRewards.TM_ULTRA, + allRewards.LEFTOVERS, + allRewards.AMULET_COIN, + allRewards.GOLDEN_PUNCH, + ], + }; + const selectRewardPhase = new SelectRewardPhase(0, undefined, customRewards); + scene.phaseManager.unshiftPhase(selectRewardPhase); + game.move.select(MoveId.SPLASH); + await game.phaseInterceptor.to("SelectRewardPhase"); + + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toEqual(RewardId.MEMORY_MUSHROOM); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toEqual(RewardId.TM_ULTRA); + expect(rewardSelectHandler.options[2].rewardOption.type.id).toEqual(RewardId.HELD_ITEM); + expect((rewardSelectHandler.options[2].rewardOption.type as HeldItemReward).itemId).toEqual(HeldItemId.LEFTOVERS); + expect(rewardSelectHandler.options[3].rewardOption.type.id).toEqual(RewardId.TRAINER_ITEM); + expect((rewardSelectHandler.options[3].rewardOption.type as TrainerItemReward).itemId).toEqual( + TrainerItemId.AMULET_COIN, + ); + expect(rewardSelectHandler.options[4].rewardOption.type.id).toEqual(RewardId.HELD_ITEM); + expect((rewardSelectHandler.options[4].rewardOption.type as HeldItemReward).itemId).toEqual( + HeldItemId.GOLDEN_PUNCH, + ); + }); + + it("should generate custom modifier tiers that can upgrade from luck", async () => { + await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); + scene.money = 1000000; + const customRewards: CustomRewardSettings = { + guaranteedRarityTiers: [ + RarityTier.COMMON, + RarityTier.GREAT, + RarityTier.ULTRA, + RarityTier.ROGUE, + RarityTier.MASTER, + ], + }; + const pokemon = new PlayerPokemon(getPokemonSpecies(SpeciesId.BULBASAUR), 10, undefined, 0, undefined, true, 2); + + // Fill party with max shinies + while (scene.getPlayerParty().length > 0) { + scene.getPlayerParty().pop(); + } + scene.getPlayerParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon); + + const selectRewardPhase = new SelectRewardPhase(0, undefined, customRewards); + scene.phaseManager.unshiftPhase(selectRewardPhase); + game.move.select(MoveId.SPLASH); + await game.phaseInterceptor.to("SelectRewardPhase"); + + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(5); + expect( + rewardSelectHandler.options[0].rewardOption.tier - rewardSelectHandler.options[0].rewardOption.upgradeCount, + ).toEqual(RarityTier.COMMON); + expect( + rewardSelectHandler.options[1].rewardOption.tier - rewardSelectHandler.options[1].rewardOption.upgradeCount, + ).toEqual(RarityTier.GREAT); + expect( + rewardSelectHandler.options[2].rewardOption.tier - rewardSelectHandler.options[2].rewardOption.upgradeCount, + ).toEqual(RarityTier.ULTRA); + expect( + rewardSelectHandler.options[3].rewardOption.tier - rewardSelectHandler.options[3].rewardOption.upgradeCount, + ).toEqual(RarityTier.ROGUE); + expect( + rewardSelectHandler.options[4].rewardOption.tier - rewardSelectHandler.options[4].rewardOption.upgradeCount, + ).toEqual(RarityTier.MASTER); + }); + + it("should generate custom modifiers and modifier tiers together", async () => { + await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); + scene.money = 1000000; + const customRewards: CustomRewardSettings = { + guaranteedRewardSpecs: [allRewards.MEMORY_MUSHROOM, allRewards.TM_COMMON], + guaranteedRarityTiers: [RarityTier.MASTER, RarityTier.MASTER], + }; + const selectRewardPhase = new SelectRewardPhase(0, undefined, customRewards); + scene.phaseManager.unshiftPhase(selectRewardPhase); + game.move.select(MoveId.SPLASH); + await game.phaseInterceptor.run(SelectRewardPhase); + + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(4); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toEqual(RewardId.MEMORY_MUSHROOM); + expect(rewardSelectHandler.options[1].rewardOption.type.id).toEqual(RewardId.TM_COMMON); + expect(rewardSelectHandler.options[2].rewardOption.tier).toEqual(RarityTier.MASTER); + expect(rewardSelectHandler.options[3].rewardOption.tier).toEqual(RarityTier.MASTER); + }); + + it("should fill remaining modifiers if fillRemaining is true with custom modifiers", async () => { + await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.VOLCARONA]); + scene.money = 1000000; + const customRewards: CustomRewardSettings = { + guaranteedRewardSpecs: [allRewards.MEMORY_MUSHROOM], + guaranteedRarityTiers: [RarityTier.MASTER], + fillRemaining: true, + }; + const selectRewardPhase = new SelectRewardPhase(0, undefined, customRewards); + scene.phaseManager.unshiftPhase(selectRewardPhase); + game.move.select(MoveId.SPLASH); + await game.phaseInterceptor.run(SelectRewardPhase); + + expect(scene.ui.getMode()).to.equal(UiMode.REWARD_SELECT); + const rewardSelectHandler = scene.ui.handlers.find( + h => h instanceof RewardSelectUiHandler, + ) as RewardSelectUiHandler; + expect(rewardSelectHandler.options.length).toEqual(3); + expect(rewardSelectHandler.options[0].rewardOption.type.id).toEqual(RewardId.MEMORY_MUSHROOM); + expect(rewardSelectHandler.options[1].rewardOption.tier).toEqual(RarityTier.MASTER); + }); +}); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index f952557bb69..17a93d759d5 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -3,7 +3,7 @@ import { BattleScene } from "#app/battle-scene"; import { getGameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; import overrides from "#app/overrides"; -import { modifierTypes } from "#data/data-lists"; +import { allRewards } from "#data/data-lists"; import { BattlerIndex } from "#enums/battler-index"; import { Button } from "#enums/buttons"; import { ExpGainsSpeed } from "#enums/exp-gains-speed"; @@ -16,7 +16,6 @@ import type { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import type { EnemyPokemon, PlayerPokemon } from "#field/pokemon"; import { Trainer } from "#field/trainer"; -import { ModifierTypeOption } from "#modifiers/modifier-type"; import { CheckSwitchPhase } from "#phases/check-switch-phase"; import { CommandPhase } from "#phases/command-phase"; import { EncounterPhase } from "#phases/encounter-phase"; @@ -49,8 +48,8 @@ import { TextInterceptor } from "#test/test-utils/text-interceptor"; import type { BallUiHandler } from "#ui/ball-ui-handler"; import type { BattleMessageUiHandler } from "#ui/battle-message-ui-handler"; import type { CommandUiHandler } from "#ui/command-ui-handler"; -import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import type { PartyUiHandler } from "#ui/party-ui-handler"; +import type { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler"; import { isNullOrUndefined } from "#utils/common"; @@ -320,13 +319,13 @@ export class GameManager { } } - /** Queue up button presses to skip taking an item on the next {@linkcode SelectModifierPhase} */ + /** Queue up button presses to skip taking an item on the next {@linkcode SelectRewardPhase} */ doSelectModifier() { this.onNextPrompt( - "SelectModifierPhase", - UiMode.MODIFIER_SELECT, + "SelectRewardPhase", + UiMode.REWARD_SELECT, () => { - const handler = this.scene.ui.getHandler() as ModifierSelectUiHandler; + const handler = this.scene.ui.getHandler() as RewardSelectUiHandler; handler.processInput(Button.CANCEL); }, () => @@ -337,10 +336,10 @@ export class GameManager { ); this.onNextPrompt( - "SelectModifierPhase", + "SelectRewardPhase", UiMode.CONFIRM, () => { - const handler = this.scene.ui.getHandler() as ModifierSelectUiHandler; + const handler = this.scene.ui.getHandler() as RewardSelectUiHandler; handler.processInput(Button.ACTION); }, () => @@ -378,7 +377,7 @@ export class GameManager { } /** - * Queue up button presses to skip taking an item on the next {@linkcode SelectModifierPhase}, + * Queue up button presses to skip taking an item on the next {@linkcode SelectRewardPhase}, * and then transition to the next {@linkcode CommandPhase}. */ async toNextWave() { @@ -490,9 +489,8 @@ export class GameManager { */ doRevivePokemon(pokemonIndex: number) { const party = this.scene.getPlayerParty(); - const candidate = new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0); - const modifier = candidate.type!.newModifier(party[pokemonIndex]); - this.scene.addModifier(modifier, false); + const reward = allRewards.MAX_REVIVE(); + reward.apply({ pokemon: party[pokemonIndex] }); } /** @@ -551,8 +549,7 @@ export class GameManager { * Removes all held items from enemy pokemon. */ removeEnemyHeldItems(): void { - this.scene.clearEnemyHeldItemModifiers(); - this.scene.clearEnemyModifiers(); + this.scene.clearEnemyItems(); console.log("Enemy held items removed"); } } diff --git a/test/test-utils/helpers/modifiers-helper.ts b/test/test-utils/helpers/modifiers-helper.ts index bfda35427fa..df0bd74f8ef 100644 --- a/test/test-utils/helpers/modifiers-helper.ts +++ b/test/test-utils/helpers/modifiers-helper.ts @@ -1,5 +1,5 @@ -import type { ModifierTypeKeys } from "#modifiers/modifier-type"; -import { itemPoolChecks } from "#modifiers/modifier-type"; +import type { RewardKeys } from "#items/reward"; +import { itemPoolChecks } from "#items/reward"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; import { expect } from "vitest"; @@ -7,11 +7,11 @@ export class ModifierHelper extends GameManagerHelper { /** * Adds a Modifier to the list of modifiers to check for. * - * Note that all modifiers are updated during the start of `SelectModifierPhase`. + * Note that all modifiers are updated during the start of `SelectRewardPhase`. * @param modifier The Modifier to add. * @returns `this` */ - addCheck(modifier: ModifierTypeKeys): this { + addCheck(modifier: RewardKeys): this { itemPoolChecks.set(modifier, undefined); return this; } @@ -23,23 +23,23 @@ export class ModifierHelper extends GameManagerHelper { * * If the item is *not* in the Modifier Pool, will return `false`. * - * If a `SelectModifierPhase` has not occurred, and we do not know if the item is in the Modifier Pool or not, will return `undefined`. + * If a `SelectRewardPhase` has not occurred, and we do not know if the item is in the Modifier Pool or not, will return `undefined`. * @param modifier * @returns */ - getCheck(modifier: ModifierTypeKeys): boolean | undefined { + getCheck(modifier: RewardKeys): boolean | undefined { return itemPoolChecks.get(modifier); } /** * `expect`s a Modifier `toBeTruthy` (in the Modifier Pool) or `Falsy` (unobtainable on this floor). Use during a test. * - * Note that if a `SelectModifierPhase` has not been run yet, these values will be `undefined`, and the check will fail. + * Note that if a `SelectRewardPhase` has not been run yet, these values will be `undefined`, and the check will fail. * @param modifier The modifier to check. * @param expectToBePreset Whether the Modifier should be in the Modifier Pool. Set to `false` to expect it to be absent instead. * @returns `this` */ - testCheck(modifier: ModifierTypeKeys, expectToBePreset: boolean): this { + testCheck(modifier: RewardKeys, expectToBePreset: boolean): this { if (expectToBePreset) { expect(itemPoolChecks.get(modifier)).toBeTruthy(); } @@ -56,4 +56,4 @@ export class ModifierHelper extends GameManagerHelper { private log(...params: any[]) { console.log("Modifiers:", ...params); } -} +} \ No newline at end of file diff --git a/test/test-utils/helpers/overrides-helper.ts b/test/test-utils/helpers/overrides-helper.ts index d67ceedf891..e02d63c1804 100644 --- a/test/test-utils/helpers/overrides-helper.ts +++ b/test/test-utils/helpers/overrides-helper.ts @@ -1,7 +1,9 @@ /** biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */ import type { NewArenaEvent } from "#events/battle-scene"; + /** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */ +import type { PokeballCounts } from "#app/battle-scene"; import type { BattleStyle, RandomTrainerOverride } from "#app/overrides"; import Overrides from "#app/overrides"; import { AbilityId } from "#enums/ability-id"; @@ -15,9 +17,11 @@ import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import type { Unlockables } from "#enums/unlockables"; import { WeatherType } from "#enums/weather-type"; -import type { ModifierOverride } from "#modifiers/modifier-type"; +import type { HeldItemConfiguration } from "#items/held-item-data-types"; +import type { TrainerItemConfiguration } from "#items/trainer-item-data-types"; import type { Variant } from "#sprites/variant"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; +import type { RewardSpecs } from "#types/rewards"; import { coerceArray, shiftCharCodes } from "#utils/common"; import { vi } from "vitest"; @@ -115,9 +119,31 @@ export class OverridesHelper extends GameManagerHelper { * @param items - The items to hold * @returns `this` */ - public startingHeldItems(items: ModifierOverride[]): this { - vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); - this.log("Player Pokemon starting held items set to:", items); + public startingHeldItems(itemConfiguration: HeldItemConfiguration): this { + vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(itemConfiguration); + this.log("Player Pokemon starting held items set to:", itemConfiguration); + return this; + } + + /** + * Override the player's starting trainer items + * @param items - The items to have + * @returns `this` + */ + public startingTrainerItems(itemConfiguration: TrainerItemConfiguration): this { + vi.spyOn(Overrides, "STARTING_TRAINER_ITEMS_OVERRIDE", "get").mockReturnValue(itemConfiguration); + this.log("Player starting trainer items set to:", itemConfiguration); + return this; + } + + /** + * Override the player's starting pokeballs + * @param items - The items to hold + * @returns `this` + */ + public startingPokeballs(pokeballs: PokeballCounts): this { + vi.spyOn(Overrides, "POKEBALL_OVERRIDE", "get").mockReturnValue({ active: true, pokeballs: pokeballs }); + this.log("Player Pokemon starting held items set to:", { active: true, pokeballs: pokeballs }); return this; } @@ -167,17 +193,6 @@ export class OverridesHelper extends GameManagerHelper { return this; } - /** - * Override the player's starting modifiers - * @param modifiers - The modifiers to set - * @returns `this` - */ - public startingModifier(modifiers: ModifierOverride[]): this { - vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers); - this.log(`Player starting modifiers set to: ${modifiers}`); - return this; - } - /** * Override the player pokemon's {@linkcode AbilityId | ability}. * @param ability - The {@linkcode AbilityId | ability} to set @@ -509,9 +524,20 @@ export class OverridesHelper extends GameManagerHelper { * @param items the items to hold * @returns `this` */ - public enemyHeldItems(items: ModifierOverride[]): this { - vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); - this.log("Enemy Pokemon held items set to:", items); + public enemyHeldItems(itemConfiguration: HeldItemConfiguration): this { + vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(itemConfiguration); + this.log("Enemy Pokemon held items set to:", itemConfiguration); + return this; + } + + /** + * Override the enemy's trainer items + * @param items - The items to have + * @returns `this` + */ + public enemyTrainerItems(itemConfiguration: TrainerItemConfiguration): this { + vi.spyOn(Overrides, "OPP_TRAINER_ITEMS_OVERRIDE", "get").mockReturnValue(itemConfiguration); + this.log("Enemy trainer items set to:", itemConfiguration); return this; } @@ -531,9 +557,9 @@ export class OverridesHelper extends GameManagerHelper { * @param items - The items to be rolled * @returns `this` */ - public itemRewards(items: ModifierOverride[]): this { - vi.spyOn(Overrides, "ITEM_REWARD_OVERRIDE", "get").mockReturnValue(items); - this.log("Item rewards set to:", items); + public rewards(items: RewardSpecs[]): this { + vi.spyOn(Overrides, "REWARD_OVERRIDE", "get").mockReturnValue(items); + this.log("Item allRewards set to:", items); return this; } diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index a8ed0e21307..3f41b13f801 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -46,16 +46,6 @@ export class ReloadHelper extends GameManagerHelper { scene.phaseManager.unshiftPhase(titlePhase); this.game.endPhase(); // End the currently ongoing battle - // remove all persistent mods before loading - // TODO: Look into why these aren't removed before load - if (this.game.scene.modifiers.length) { - console.log( - "Removing %d modifiers from scene on load...", - this.game.scene.modifiers.length, - this.game.scene.modifiers, - ); - this.game.scene.modifiers = []; - } titlePhase.loadSaveSlot(-1); // Load the desired session data this.game.phaseInterceptor.shift(); // Loading the save slot also ended TitlePhase, clean it up diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 50de7e9f047..2944b4d37c8 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -14,13 +14,12 @@ import { EvolutionPhase } from "#phases/evolution-phase"; import { ExpPhase } from "#phases/exp-phase"; import { FaintPhase } from "#phases/faint-phase"; import { FormChangePhase } from "#phases/form-change-phase"; -import { GameOverModifierRewardPhase } from "#phases/game-over-modifier-reward-phase"; import { GameOverPhase } from "#phases/game-over-phase"; +import { GameOverRewardPhase } from "#phases/game-over-reward-phase"; import { LearnMovePhase } from "#phases/learn-move-phase"; import { LevelCapPhase } from "#phases/level-cap-phase"; import { LoginPhase } from "#phases/login-phase"; import { MessagePhase } from "#phases/message-phase"; -import { ModifierRewardPhase } from "#phases/modifier-reward-phase"; import { MoveEffectPhase } from "#phases/move-effect-phase"; import { MoveEndPhase } from "#phases/move-end-phase"; import { MovePhase } from "#phases/move-phase"; @@ -42,10 +41,11 @@ import { PostGameOverPhase } from "#phases/post-game-over-phase"; import { PostSummonPhase } from "#phases/post-summon-phase"; import { QuietFormChangePhase } from "#phases/quiet-form-change-phase"; import { RevivalBlessingPhase } from "#phases/revival-blessing-phase"; -import { RibbonModifierRewardPhase } from "#phases/ribbon-modifier-reward-phase"; +import { RewardPhase } from "#phases/reward-phase"; +import { RibbonRewardPhase } from "#phases/ribbon-reward-phase"; import { SelectBiomePhase } from "#phases/select-biome-phase"; import { SelectGenderPhase } from "#phases/select-gender-phase"; -import { SelectModifierPhase } from "#phases/select-modifier-phase"; +import { SelectRewardPhase } from "#phases/select-reward-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import { SelectTargetPhase } from "#phases/select-target-phase"; import { ShinySparklePhase } from "#phases/shiny-sparkle-phase"; @@ -123,7 +123,7 @@ export class PhaseInterceptor { [TurnEndPhase, this.startPhase], [BattleEndPhase, this.startPhase], [EggLapsePhase, this.startPhase], - [SelectModifierPhase, this.startPhase], + [SelectRewardPhase, this.startPhase], [NextEncounterPhase, this.startPhase], [NewBattlePhase, this.startPhase], [VictoryPhase, this.startPhase], @@ -150,9 +150,9 @@ export class PhaseInterceptor { [MysteryEncounterBattlePhase, this.startPhase], [MysteryEncounterRewardsPhase, this.startPhase], [PostMysteryEncounterPhase, this.startPhase], - [RibbonModifierRewardPhase, this.startPhase], - [GameOverModifierRewardPhase, this.startPhase], - [ModifierRewardPhase, this.startPhase], + [RibbonRewardPhase, this.startPhase], + [GameOverRewardPhase, this.startPhase], + [RewardPhase, this.startPhase], [PartyExpPhase, this.startPhase], [ExpPhase, this.startPhase], [EncounterPhase, this.startPhase], @@ -166,7 +166,7 @@ export class PhaseInterceptor { TitlePhase, SelectGenderPhase, CommandPhase, - SelectModifierPhase, + SelectRewardPhase, MysteryEncounterPhase, PostMysteryEncounterPhase, ]; diff --git a/test/ui/transfer-item.test.ts b/test/ui/transfer-item.test.ts index 0d101b5b4ef..5b67c38a609 100644 --- a/test/ui/transfer-item.test.ts +++ b/test/ui/transfer-item.test.ts @@ -1,11 +1,11 @@ -import { BerryType } from "#enums/berry-type"; import { Button } from "#enums/buttons"; +import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; +import { RewardSelectUiHandler } from "#ui/reward-select-ui-handler"; import Phaser from "phaser"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -31,9 +31,9 @@ describe("UI - Transfer Items", () => { .startingLevel(100) .startingWave(1) .startingHeldItems([ - { name: "BERRY", count: 1, type: BerryType.SITRUS }, - { name: "BERRY", count: 2, type: BerryType.APICOT }, - { name: "BERRY", count: 2, type: BerryType.LUM }, + { entry: HeldItemId.SITRUS_BERRY, count: 1 }, + { entry: HeldItemId.APICOT_BERRY, count: 2 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ]) .moveset([MoveId.DRAGON_CLAW]) .enemySpecies(SpeciesId.MAGIKARP) @@ -43,21 +43,21 @@ describe("UI - Transfer Items", () => { game.move.select(MoveId.DRAGON_CLAW); - game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, () => { - expect(game.scene.ui.getHandler()).toBeInstanceOf(ModifierSelectUiHandler); + game.onNextPrompt("SelectRewardPhase", UiMode.REWARD_SELECT, () => { + expect(game.scene.ui.getHandler()).toBeInstanceOf(RewardSelectUiHandler); - const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler; + const handler = game.scene.ui.getHandler() as RewardSelectUiHandler; handler.setCursor(1); handler.processInput(Button.ACTION); - void game.scene.ui.setModeWithoutClear(UiMode.PARTY, PartyUiMode.MODIFIER_TRANSFER); + void game.scene.ui.setModeWithoutClear(UiMode.PARTY, PartyUiMode.ITEM_TRANSFER); }); await game.phaseInterceptor.to("BattleEndPhase"); }); it("check red tint for held item limit in transfer menu", async () => { - game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, () => { + game.onNextPrompt("SelectRewardPhase", UiMode.PARTY, () => { expect(game.scene.ui.getHandler()).toBeInstanceOf(PartyUiHandler); const handler = game.scene.ui.getHandler() as PartyUiHandler; @@ -76,11 +76,11 @@ describe("UI - Transfer Items", () => { game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.to("SelectModifierPhase"); + await game.phaseInterceptor.to("SelectRewardPhase"); }); it("check transfer option for pokemon to transfer to", async () => { - game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, () => { + game.onNextPrompt("SelectRewardPhase", UiMode.PARTY, () => { expect(game.scene.ui.getHandler()).toBeInstanceOf(PartyUiHandler); const handler = game.scene.ui.getHandler() as PartyUiHandler; @@ -97,6 +97,6 @@ describe("UI - Transfer Items", () => { game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.to("SelectModifierPhase"); + await game.phaseInterceptor.to("SelectRewardPhase"); }); }); diff --git a/tsconfig.json b/tsconfig.json index dcbf7456df8..471c1034996 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,7 +30,7 @@ "#events/*": ["./events/*.ts"], "#field/*": ["./field/*.ts"], "#inputs/*": ["./configs/inputs/*.ts"], - "#modifiers/*": ["./modifier/*.ts"], + "#items/*": ["./items/held-items/*.ts", "./items/*.ts"], "#moves/*": ["./data/moves/*.ts"], "#mystery-encounters/*": [ "./data/mystery-encounters/utils/*.ts",