diff --git a/src/battle-scene.ts b/src/battle-scene.ts index cbaf07d579c..ee0b83a8714 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -19,7 +19,7 @@ import { type Constructor, } from "#app/utils/common"; import { deepMergeSpriteData } from "#app/utils/data"; -import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier"; +import type { Modifier, ModifierPredicate } from "./modifier/modifier"; import { ConsumableModifier, ConsumablePokemonModifier, @@ -28,14 +28,9 @@ import { ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, - ModifierBar, MultipleParticipantExpBonusModifier, PersistentModifier, - PokemonExpBoosterModifier, - PokemonFormChangeItemModifier, - PokemonHeldItemModifier, PokemonHpRestoreModifier, - PokemonIncrementingStatModifier, RememberMoveModifier, } from "./modifier/modifier"; import { PokeballType } from "#enums/pokeball"; @@ -53,18 +48,13 @@ import { GameData } from "#app/system/game-data"; import { addTextObject, getTextColor, TextStyle } from "#app/ui/text"; import { allMoves } from "./data/data-lists"; import { MusicPreference } from "#app/system/settings/settings"; +import { getModifierType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, - getLuckString, - getLuckTextTint, getModifierPoolForType, - getModifierType, - getPartyLuckValue, - ModifierPoolType, - modifierTypes, - PokemonHeldItemModifierType, -} from "#app/modifier/modifier-type"; +} from "#app/modifier/modifier-pool"; +import { getLuckString, getLuckTextTint, getPartyLuckValue } from "#app/modifier/modifier-utils"; import AbilityBar from "#app/ui/ability-bar"; import { applyAbAttrs, @@ -186,6 +176,15 @@ import { hasExpSprite } from "./sprites/sprite-utils"; import { timedEventManager } from "./global-event-manager"; import { starterColors } from "./global-vars/starter-colors"; import { startingWave } from "./starting-wave"; +import { ModifierBar } from "./modifier/modifier-bar"; +import { + PokemonExpBoosterModifier, + PokemonFormChangeItemModifier, + PokemonHeldItemModifier, + PokemonIncrementingStatModifier, + type TurnHeldItemTransferModifier, +} from "./modifier/held-item-modifier"; +import { ModifierPoolType } from "./modifier/modifier-pool-type"; const DEBUG_RNG = false; diff --git a/src/battle.ts b/src/battle.ts index 07e520d6bc0..9e83945f364 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -11,7 +11,7 @@ import { } from "#app/utils/common"; import Trainer, { TrainerVariant } from "./field/trainer"; import type { GameMode } from "./game-mode"; -import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; +import { MoneyMultiplierModifier } from "./modifier/modifier"; import type { PokeballType } from "#enums/pokeball"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { SpeciesFormKey } from "#enums/species-form-key"; @@ -27,11 +27,12 @@ import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import type { CustomModifierSettings } from "#app/modifier/modifier-type"; +import type { CustomModifierSettings } from "#app/modifier/modifier-pool"; import { ModifierTier } from "#app/modifier/modifier-tier"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { BattleType } from "#enums/battle-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; +import { PokemonHeldItemModifier } from "./modifier/held-item-modifier"; export enum BattlerIndex { ATTACKER = -1, diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index f49863639f0..5440726e0eb 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -22,7 +22,7 @@ import { } from "#app/data/moves/move"; import { allMoves } from "../data-lists"; import { ArenaTagSide } from "#app/data/arena-tag"; -import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; import { TerrainType } from "#app/data/terrain"; import { SpeciesFormChangeAbilityTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms"; import i18next from "i18next"; diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index cf1e4061987..aaa6ad7b7bf 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -11,11 +11,12 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { SpeciesFormKey } from "#enums/species-form-key"; import { TimeOfDay } from "#enums/time-of-day"; -import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier, SpeciesStatBoosterModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; +import { ExtraModifierModifier, MoneyMultiplierModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; import type { SpeciesStatBoosterModifierType } from "#app/modifier/modifier-type"; import { speciesStarterCosts } from "./starters"; import i18next from "i18next"; import { initI18n } from "#app/plugins/i18n"; +import { DamageMoneyRewardModifier, SpeciesStatBoosterModifier } from "#app/modifier/held-item-modifier"; export enum SpeciesWildEvolutionDelay { NONE, diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 8a0da5f35c2..83f4bbda1de 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -73,12 +73,11 @@ import { PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, - PreserveBerryModifier, -} from "../../modifier/modifier"; +} from "../../modifier/held-item-modifier"; import type { BattlerIndex } from "../../battle"; import { BattleType } from "#enums/battle-type"; import { TerrainType } from "../terrain"; -import { ModifierPoolType } from "#app/modifier/modifier-type"; +import { getOrInferTier } from "#app/modifier/modifier-pool"; import { Command } from "../../ui/command-ui-handler"; import i18next from "i18next"; import type { Localizable } from "#app/interfaces/locales"; @@ -123,6 +122,8 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger"; import { MultiHitType } from "#enums/MultiHitType"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves"; import { SelectBiomePhase } from "#app/phases/select-biome-phase"; +import { PreserveBerryModifier } from "#app/modifier/modifier"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -2558,8 +2559,8 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { } 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 highestItemTier = heldItems.map((m) => getOrInferTier(m.type, poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct? + const tierHeldItems = heldItems.filter((m) => getOrInferTier(m.type, poolType) === highestItemTier); const stolenItem = tierHeldItems[user.randBattleSeedInt(tierHeldItems.length)]; if (!globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) { return false; diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index acfc8cb16a1..72e1c30d632 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -20,7 +20,7 @@ import { PersistentModifierRequirement } from "#app/data/mystery-encounters/myst import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { BerryModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier"; +import { BerryModifier, PokemonInstantReviveModifier } from "#app/modifier/held-item-modifier"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Moves } from "#enums/moves"; import { BattlerTagType } from "#enums/battler-tag-type"; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 7f54e51565e..56ef2bace2f 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -12,7 +12,8 @@ import { import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import type { BerryModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; -import { ModifierPoolType, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-pool"; import { randSeedInt } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -31,12 +32,13 @@ import { STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER, } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; -import { BerryModifier } from "#app/modifier/modifier"; +import { BerryModifier } from "#app/modifier/held-item-modifier"; import i18next from "#app/plugins/i18n"; import { BerryType } from "#enums/berry-type"; import { PERMANENT_STATS, Stat } from "#enums/stat"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/berriesAbound"; 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 17c1c31d55e..a5dd982b7f2 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -40,14 +40,13 @@ import { import { PokemonType } from "#enums/pokemon-type"; import type { AttackTypeBoosterModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { AttackTypeBoosterModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, - GigantamaxAccessModifier, - MegaEvolutionAccessModifier, -} from "#app/modifier/modifier"; + type PokemonHeldItemModifier, +} from "#app/modifier/held-item-modifier"; +import { GigantamaxAccessModifier, MegaEvolutionAccessModifier } from "#app/modifier/modifier"; import i18next from "i18next"; import MoveInfoOverlay from "#app/ui/move-info-overlay"; import { allMoves } from "#app/data/data-lists"; diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index ce5eb2cfdd1..2e21ee6d56f 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -13,7 +13,8 @@ import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTem import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { ModifierTier } from "#app/modifier/modifier-tier"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { ModifierPoolType, modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { globalScene } from "#app/global-scene"; @@ -39,7 +40,7 @@ import type { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handle import type { PlayerPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/field/pokemon"; import { Ability } from "#app/data/abilities/ability-class"; -import { BerryModifier } from "#app/modifier/modifier"; +import { BerryModifier } from "#app/modifier/held-item-modifier"; import { BerryType } from "#enums/berry-type"; import { BattlerIndex } from "#app/battle"; import { Moves } from "#enums/moves"; @@ -49,6 +50,7 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { EncounterAnim } from "#enums/encounter-anims"; import { Challenges } from "#enums/challenges"; +import { withTierFromPool } from "#app/modifier/modifier-pool"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/clowningAround"; @@ -318,7 +320,7 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder let numRogue = 0; for (const m of items.filter(m => m.isTransferable && !(m instanceof BerryModifier))) { - const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party); + const type = withTierFromPool(m.type, ModifierPoolType.PLAYER, party); const tier = type.tier ?? ModifierTier.ULTRA; if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { numRogue += m.stackCount; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index e746b13c6a5..18b1d1d613d 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -17,8 +17,8 @@ import { import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { PokemonFormChangeItemModifier } from "#app/modifier/modifier"; +import type { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; +import { PokemonFormChangeItemModifier } from "#app/modifier/held-item-modifier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { Challenges } from "#enums/challenges"; diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 7040bb47d19..5882df947c2 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -19,9 +19,12 @@ import { getPokemonSpecies } from "#app/data/pokemon-species"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; -import type { PokemonHeldItemModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { BerryModifier, + type PokemonHeldItemModifier, + type PokemonInstantReviveModifier, +} from "#app/modifier/held-item-modifier"; +import { HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, 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 ecc2e17a06f..c978932f990 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -11,11 +11,7 @@ import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requir import type Pokemon from "#app/field/pokemon"; import { ModifierTier } from "#app/modifier/modifier-tier"; import type { ModifierTypeOption } from "#app/modifier/modifier-type"; -import { - getPlayerModifierTypeOptions, - ModifierPoolType, - regenerateModifierPoolThresholds, -} from "#app/modifier/modifier-type"; +import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-pool"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -34,6 +30,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun import { randSeedInt } from "#app/utils/common"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/fightOrFlight"; 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 bb41bc7883c..bfcb176c4c9 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -7,11 +7,7 @@ import { TrainerSlot } from "#enums/trainer-slot"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { MusicPreference } from "#app/system/settings/settings"; import type { ModifierTypeOption } from "#app/modifier/modifier-type"; -import { - getPlayerModifierTypeOptions, - ModifierPoolType, - regenerateModifierPoolThresholds, -} from "#app/modifier/modifier-type"; +import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-pool"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -34,13 +30,12 @@ import { import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { - HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, - ShinyRateBoosterModifier, SpeciesStatBoosterModifier, -} from "#app/modifier/modifier"; + type PokemonHeldItemModifier, +} from "#app/modifier/held-item-modifier"; +import { HiddenAbilityRateBoosterModifier, ShinyRateBoosterModifier } from "#app/modifier/modifier"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import PokemonData from "#app/system/pokemon-data"; import i18next from "i18next"; @@ -54,6 +49,8 @@ import type { PokeballType } from "#enums/pokeball"; import { doShinySparkleAnim } from "#app/field/anims"; import { TrainerType } from "#enums/trainer-type"; import { timedEventManager } from "#app/global-event-manager"; +import { withTierFromPool } from "#app/modifier/modifier-pool"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/globalTradeSystem"; @@ -437,7 +434,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil 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); + const type = withTierFromPool(modifier.type, ModifierPoolType.PLAYER, party); let tier = type.tier ?? ModifierTier.GREAT; // Eggs and White Herb are not in the pool if (type.id === "WHITE_HERB") { diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index 28c7fe4644f..06e8ae6eba7 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -23,7 +23,8 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode import { Biome } from "#enums/biome"; import { getBiomeKey } from "#app/field/arena"; import { PokemonType } from "#enums/pokemon-type"; -import { getPartyLuckValue, modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { getPartyLuckValue } from "#app/modifier/modifier-utils"; import { TrainerSlot } from "#enums/trainer-slot"; import { BattlerTagType } from "#enums/battler-tag-type"; import { getPokemonNameWithAffix } from "#app/messages"; diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 597a6b009b3..08be8ac4c18 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -11,7 +11,7 @@ import { getNatureName } from "#app/data/nature"; import { speciesStarterCosts } from "#app/data/balance/starters"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import type { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; import { AbilityAttr } from "#app/system/game-data"; import PokemonData from "#app/system/pokemon-data"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; 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 1e1db14705a..3a1c4b5ca85 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -17,7 +17,7 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Species } from "#enums/species"; -import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#app/modifier/modifier"; +import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#app/modifier/held-item-modifier"; import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import i18next from "#app/plugins/i18n"; diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index f4eec5b0923..50efdc8db06 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -34,7 +34,7 @@ import { SelfStatusMove } from "#app/data/moves/move"; import { PokeballType } from "#enums/pokeball"; import { BattlerTagType } from "#enums/battler-tag-type"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { BerryModifier } from "#app/modifier/modifier"; +import { BerryModifier } from "#app/modifier/held-item-modifier"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index cceda25fcb4..9528d19ed42 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -20,8 +20,8 @@ import { PokemonMove } from "#app/field/pokemon"; import { NumberHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils/common"; import type PokemonSpecies from "#app/data/pokemon-species"; import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#app/modifier/modifier"; +import { PokemonFormChangeItemModifier, type PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; +import { HiddenAbilityRateBoosterModifier } from "#app/modifier/modifier"; import { achvs } from "#app/system/achv"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 49fd632932c..aea2e333854 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -7,7 +7,7 @@ import { StatusEffect } from "#enums/status-effect"; import { PokemonType } from "#enums/pokemon-type"; import { WeatherType } from "#enums/weather-type"; import type { PlayerPokemon } from "#app/field/pokemon"; -import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; +import { AttackTypeBoosterModifier } from "#app/modifier/held-item-modifier"; import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; import { isNullOrUndefined } from "#app/utils/common"; import type { Abilities } from "#enums/abilities"; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 0215928bbe8..633a6f22f20 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -11,15 +11,18 @@ import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter- import type { AiType, PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { EnemyPokemon, FieldPosition, PokemonMove } from "#app/field/pokemon"; -import type { CustomModifierSettings, ModifierType } from "#app/modifier/modifier-type"; import { - getPartyLuckValue, - ModifierPoolType, + type ModifierType, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, - regenerateModifierPoolThresholds, } from "#app/modifier/modifier-type"; +import { + type CustomModifierSettings, + regenerateModifierPoolThresholds, + withTierFromPool, +} from "#app/modifier/modifier-pool"; +import { getPartyLuckValue } from "#app/modifier/modifier-utils"; import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, @@ -66,6 +69,7 @@ import { PokemonType } from "#enums/pokemon-type"; import { getNatureName } from "#app/data/nature"; import { getPokemonNameWithAffix } from "#app/messages"; import { timedEventManager } from "#app/global-event-manager"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; /** * Animates exclamation sprite over trainer's head at start of encounter @@ -513,9 +517,11 @@ export function generateModifierType(modifier: () => ModifierType, pregenArgs?: let result: ModifierType = modifierTypes[modifierId](); // Populates item id and tier (order matters) - result = result - .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(ModifierPoolType.PLAYER, globalScene.getPlayerParty()); + result = withTierFromPool( + result.withIdFromFunc(modifierTypes[modifierId]), + ModifierPoolType.PLAYER, + globalScene.getPlayerParty(), + ); return result instanceof ModifierTypeGenerator ? result.generateType(globalScene.getPlayerParty(), pregenArgs) diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index a6a87b4ab9a..d94cb0b7a93 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import i18next from "i18next"; import { isNullOrUndefined, randSeedInt } from "#app/utils/common"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index da594f7c27f..cc09af4b0d7 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -1,4 +1,4 @@ -import { PokemonFormChangeItemModifier } from "../modifier/modifier"; +import { PokemonFormChangeItemModifier } from "../modifier/held-item-modifier"; import type Pokemon from "../field/pokemon"; import { StatusEffect } from "#enums/status-effect"; import { allMoves } from "./data-lists"; diff --git a/src/events/battle-scene.ts b/src/events/battle-scene.ts index 83d260bd7d2..085619201bc 100644 --- a/src/events/battle-scene.ts +++ b/src/events/battle-scene.ts @@ -1,5 +1,5 @@ import type Move from "../data/moves/move"; -import type { BerryModifier } from "../modifier/modifier"; +import type { BerryModifier } from "../modifier/held-item-modifier"; /** Alias for all {@linkcode BattleScene} events */ export enum BattleSceneEventType { diff --git a/src/field/arena.ts b/src/field/arena.ts index f083180490b..b58bcaaf94a 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -40,7 +40,7 @@ import { Abilities } from "#enums/abilities"; import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { WeatherType } from "#enums/weather-type"; -import { FieldEffectModifier } from "#app/modifier/modifier"; +import { FieldEffectModifier } from "#app/modifier/held-item-modifier"; export class Arena { public biomeType: Biome; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 329ba06fd09..68746c74f9f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -87,14 +87,16 @@ import { EnemyDamageReducerModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, + ShinyRateBoosterModifier, + TempStatStageBoosterModifier, + TempCritBoosterModifier, +} from "#app/modifier/modifier"; +import { BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, - ShinyRateBoosterModifier, SurviveDamageModifier, - TempStatStageBoosterModifier, - TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, PokemonBaseStatFlatModifier, @@ -102,7 +104,7 @@ import { PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier, -} from "#app/modifier/modifier"; +} from "#app/modifier/held-item-modifier"; import { PokeballType } from "#enums/pokeball"; import { Gender } from "#app/data/gender"; import { Status, getRandomStatus } from "#app/data/status-effect"; @@ -6702,7 +6704,6 @@ export class EnemyPokemon extends Pokemon { return ret; } - /** * Show or hide the type effectiveness multiplier window * Passing undefined will hide the window diff --git a/src/interfaces/held-modifier-config.ts b/src/interfaces/held-modifier-config.ts index 5617cf2446a..3c48aa3849a 100644 --- a/src/interfaces/held-modifier-config.ts +++ b/src/interfaces/held-modifier-config.ts @@ -1,5 +1,5 @@ import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import type { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; export default interface HeldModifierConfig { modifier: PokemonHeldItemModifierType | PokemonHeldItemModifier; diff --git a/src/modifier/held-item-modifier.ts b/src/modifier/held-item-modifier.ts new file mode 100644 index 00000000000..4327d4c5717 --- /dev/null +++ b/src/modifier/held-item-modifier.ts @@ -0,0 +1,1906 @@ +import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { BooleanHolder, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils/common"; +import { Color, ShadowColor } from "#enums/color"; +import type { PokemonType } from "#enums/pokemon-type"; +import type { Species } from "#enums/species"; +import { BATTLE_STATS, type PermanentStat, Stat } from "#enums/stat"; +import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { Command } from "#app/ui/command-ui-handler"; +import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; +import { StatusEffect } from "#enums/status-effect"; +import { BerryType } from "#enums/berry-type"; +import { getBerryEffectFunc, getBerryPredicate } from "#app/data/berry"; +import { + applyAbAttrs, + applyPostItemLostAbAttrs, + CommanderAbAttr, + PostItemLostAbAttr, +} from "#app/data/abilities/ability"; +import type { Moves } from "#enums/moves"; +import { allMoves } from "#app/data/data-lists"; +import { type FormChangeItem, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; +import { + ExtraModifierModifier, + type Modifier, + MoneyMultiplierModifier, + PersistentModifier, + PreserveBerryModifier, + TempExtraModifierModifier, +} from "./modifier"; +import type { + FormChangeItemModifierType, + PokemonExpBoosterModifierType, + PokemonFriendshipBoosterModifierType, + PokemonMoveAccuracyBoosterModifierType, + PokemonMultiHitModifierType, + ModifierType, + PokemonBaseStatTotalModifierType, +} from "./modifier-type"; +import { getOrInferTier } from "./modifier-pool"; +import { ModifierPoolType } from "./modifier-pool-type"; + +export abstract class PokemonHeldItemModifier extends PersistentModifier { + /** The ID of the {@linkcode Pokemon} that this item belongs to. */ + public pokemonId: number; + /** Whether this item can be transfered to or stolen by another Pokemon. */ + public isTransferable = true; + + constructor(type: ModifierType, pokemonId: number, stackCount?: number) { + super(type, stackCount); + + this.pokemonId = pokemonId; + } + + abstract matchType(_modifier: Modifier): boolean; + + match(modifier: Modifier) { + return this.matchType(modifier) && (modifier as PokemonHeldItemModifier).pokemonId === this.pokemonId; + } + + getArgs(): any[] { + return [this.pokemonId]; + } + + /** + * Applies the {@linkcode PokemonHeldItemModifier} to the given {@linkcode Pokemon}. + * @param pokemon The {@linkcode Pokemon} that holds the held item + * @param args additional parameters + */ + abstract override apply(pokemon: Pokemon, ...args: unknown[]): boolean; + + /** + * Checks if {@linkcode PokemonHeldItemModifier} should be applied. + * @param pokemon The {@linkcode Pokemon} that holds the item + * @param _args N/A + * @returns if {@linkcode PokemonHeldItemModifier} should be applied + */ + override shouldApply(pokemon?: Pokemon, ..._args: unknown[]): boolean { + return !!pokemon && (this.pokemonId === -1 || pokemon.id === this.pokemonId); + } + + isIconVisible(): boolean { + return !!this.getPokemon()?.isOnField(); + } + + getIcon(forSummary?: boolean): Phaser.GameObjects.Container { + const container = !forSummary ? globalScene.add.container(0, 0) : super.getIcon(); + + if (!forSummary) { + const pokemon = this.getPokemon(); + if (pokemon) { + const pokemonIcon = globalScene.addPokemonIcon(pokemon, -2, 10, 0, 0.5, undefined, true); + container.add(pokemonIcon); + container.setName(pokemon.id.toString()); + } + + const item = globalScene.add.sprite(16, this.virtualStackCount ? 8 : 16, "items"); + item.setScale(0.5); + item.setOrigin(0, 0.5); + item.setTexture("items", this.type.iconImage); + container.add(item); + + const stackText = this.getIconStackText(); + if (stackText) { + container.add(stackText); + } + + const virtualStackText = this.getIconStackText(true); + if (virtualStackText) { + container.add(virtualStackText); + } + } else { + container.setScale(0.5); + } + + return container; + } + + getPokemon(): Pokemon | undefined { + return this.pokemonId ? (globalScene.getPokemonById(this.pokemonId) ?? undefined) : undefined; + } + + getScoreMultiplier(): number { + return 1; + } + + getMaxStackCount(forThreshold?: boolean): number { + const pokemon = this.getPokemon(); + if (!pokemon) { + return 0; + } + if (pokemon.isPlayer() && forThreshold) { + return globalScene + .getPlayerParty() + .map(p => this.getMaxHeldItemCount(p)) + .reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0); + } + return this.getMaxHeldItemCount(pokemon); + } + + 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: Species; + protected required: number; + public isTransferable = false; + + constructor(type: ModifierType, pokemonId: number, species: Species, 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 { + if (this.getMaxStackCount() === 1 || (virtual && !this.virtualStackCount)) { + return null; + } + + const pokemon = globalScene.getPokemonById(this.pokemonId); + + this.stackCount = pokemon + ? pokemon.evoCounter + + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + + globalScene.findModifiers( + m => + m instanceof MoneyMultiplierModifier || + m instanceof ExtraModifierModifier || + m instanceof TempExtraModifierModifier, + ).length + : this.stackCount; + + const text = globalScene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); + text.letterSpacing = -0.5; + if (this.getStackCount() >= this.required) { + text.setTint(0xf89890); + } + text.setOrigin(0, 0); + + return text; + } + + getMaxHeldItemCount(pokemon: Pokemon): number { + this.stackCount = + pokemon.evoCounter + + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + + globalScene.findModifiers( + m => + m instanceof MoneyMultiplierModifier || + m instanceof ExtraModifierModifier || + m instanceof TempExtraModifierModifier, + ).length; + return 999; + } +} + +/** + * Currently used by Shuckle Juice item + */ +export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { + public override type: PokemonBaseStatTotalModifierType; + public isTransferable = false; + + private statModifier: number; + + constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: number, stackCount?: number) { + super(type, pokemonId, stackCount); + this.statModifier = statModifier; + } + + override matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonBaseStatTotalModifier && this.statModifier === modifier.statModifier; + } + + override clone(): PersistentModifier { + return new PokemonBaseStatTotalModifier(this.type, this.pokemonId, this.statModifier, this.stackCount); + } + + override getArgs(): any[] { + return super.getArgs().concat(this.statModifier); + } + + /** + * Checks if {@linkcode PokemonBaseStatTotalModifier} should be applied to the specified {@linkcode Pokemon}. + * @param pokemon the {@linkcode Pokemon} to be modified + * @param baseStats the base stats of the {@linkcode Pokemon} + * @returns `true` if the {@linkcode Pokemon} should be modified + */ + override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { + return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); + } + + /** + * Applies the {@linkcode PokemonBaseStatTotalModifier} + * @param _pokemon the {@linkcode Pokemon} to be modified + * @param baseStats the base stats of the {@linkcode Pokemon} + * @returns always `true` + */ + override apply(_pokemon: Pokemon, baseStats: number[]): boolean { + // Modifies the passed in baseStats[] array + baseStats.forEach((v, i) => { + // HP is affected by half as much as other stats + const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier); + baseStats[i] = Math.min(Math.max(newVal, 1), 999999); + }); + + return true; + } + + override getScoreMultiplier(): number { + return 1.2; + } + + override getMaxHeldItemCount(_pokemon: Pokemon): number { + return 2; + } +} + +/** + * Currently used by Old Gateau item + */ +export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { + private statModifier: number; + private stats: Stat[]; + public isTransferable = false; + + constructor(type: ModifierType, pokemonId: number, statModifier: number, stats: Stat[], stackCount?: number) { + super(type, pokemonId, stackCount); + + this.statModifier = statModifier; + this.stats = stats; + } + + override matchType(modifier: Modifier): boolean { + return ( + modifier instanceof PokemonBaseStatFlatModifier && + modifier.statModifier === this.statModifier && + this.stats.every(s => modifier.stats.some(stat => s === stat)) + ); + } + + override clone(): PersistentModifier { + return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount); + } + + override getArgs(): any[] { + return [...super.getArgs(), this.statModifier, this.stats]; + } + + /** + * Checks if the {@linkcode PokemonBaseStatFlatModifier} should be applied to the {@linkcode Pokemon}. + * @param pokemon The {@linkcode Pokemon} that holds the item + * @param baseStats The base stats of the {@linkcode Pokemon} + * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied + */ + override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { + return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); + } + + /** + * Applies the {@linkcode PokemonBaseStatFlatModifier} + * @param _pokemon The {@linkcode Pokemon} that holds the item + * @param baseStats The base stats of the {@linkcode Pokemon} + * @returns always `true` + */ + override apply(_pokemon: Pokemon, baseStats: number[]): boolean { + // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats + baseStats.forEach((v, i) => { + if (this.stats.includes(i)) { + const newVal = Math.floor(v + this.statModifier); + baseStats[i] = Math.min(Math.max(newVal, 1), 999999); + } + }); + + return true; + } + + override getScoreMultiplier(): number { + return 1.1; + } + + override getMaxHeldItemCount(_pokemon: Pokemon): number { + return 1; + } +} + +/** + * Currently used by Macho Brace item + */ +export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { + public isTransferable = false; + + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonIncrementingStatModifier; + } + + clone(): PokemonIncrementingStatModifier { + return new PokemonIncrementingStatModifier(this.type, this.pokemonId, this.stackCount); + } + + getArgs(): any[] { + return super.getArgs(); + } + + /** + * 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 { + clone() { + return super.clone() as EvolutionStatBoosterModifier; + } + + 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 Species}. + * @extends StatBoosterModifier + * @see {@linkcode apply} + */ +export class SpeciesStatBoosterModifier extends StatBoosterModifier { + /** The species that the held item's stat boost(s) apply to */ + private species: Species[]; + + constructor( + type: ModifierType, + pokemonId: number, + stats: Stat[], + multiplier: number, + species: Species[], + 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 Species} + * (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 Species} 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: Species, 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 Species}. + * @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: Species[]; + + constructor(type: ModifierType, pokemonId: number, stageIncrement: number, species: Species[], 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 Species} (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.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.id === "QUICK_CLAW"; + + if (isCommandFight && hasQuickClaw) { + globalScene.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 Abilities.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.unshiftPhase( + new 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.unshiftPhase( + new 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 BerryModifier extends PokemonHeldItemModifier { + public berryType: BerryType; + public consumed: boolean; + + constructor(type: ModifierType, pokemonId: number, berryType: BerryType, stackCount?: number) { + super(type, pokemonId, stackCount); + + this.berryType = berryType; + this.consumed = false; + } + + matchType(modifier: Modifier) { + return modifier instanceof BerryModifier && (modifier as BerryModifier).berryType === this.berryType; + } + + clone() { + return new BerryModifier(this.type, this.pokemonId, this.berryType, this.stackCount); + } + + getArgs(): any[] { + return super.getArgs().concat(this.berryType); + } + + /** + * Checks if {@linkcode BerryModifier} should be applied + * @param pokemon The {@linkcode Pokemon} that holds the berry + * @returns `true` if {@linkcode BerryModifier} should be applied + */ + override shouldApply(pokemon: Pokemon): boolean { + return !this.consumed && super.shouldApply(pokemon) && getBerryPredicate(this.berryType)(pokemon); + } + + /** + * Applies {@linkcode BerryModifier} + * @param pokemon The {@linkcode Pokemon} that holds the berry + * @returns always `true` + */ + override apply(pokemon: Pokemon): boolean { + const preserve = new BooleanHolder(false); + globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); + this.consumed = !preserve.value; + + // munch the berry and trigger unburden-like effects + getBerryEffectFunc(this.berryType)(pokemon); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); + + // Update berry eaten trackers for Belch, Harvest, Cud Chew, etc. + // Don't recover it if we proc berry pouch (no item duplication) + pokemon.recordEatenBerry(this.berryType, this.consumed); + + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + if ([BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(this.berryType)) { + return 2; + } + return 3; + } +} + +export class 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.unshiftPhase( + new PokemonHealPhase( + pokemon.getBattlerIndex(), + toDmgValue(pokemon.getMaxHp() / 2), + i18next.t("modifier:pokemonInstantReviveApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.type.name, + }), + false, + false, + true, + ), + ); + + // Remove the Pokemon's FAINT status + pokemon.resetStatus(true, false, true, false); + + // Reapply Commander on the Pokemon's side of the field, if applicable + const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); + for (const p of field) { + applyAbAttrs(CommanderAbAttr, p, null, false); + } + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + return 1; + } +} + +/** + * Modifier used for held items, namely White Herb, that restore adverse stat + * stages in battle. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier { + matchType(modifier: Modifier) { + return modifier instanceof ResetNegativeStatStageModifier; + } + + clone() { + return new ResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount); + } + + /** + * Goes through the holder's stat stages and, if any are negative, resets that + * stat stage back to 0. + * @param pokemon {@linkcode Pokemon} that holds the item + * @returns `true` if any stat stages were reset, false otherwise + */ + override apply(pokemon: Pokemon): boolean { + let statRestored = false; + + for (const s of BATTLE_STATS) { + if (pokemon.getStatStage(s) < 0) { + pokemon.setStatStage(s, 0); + statRestored = true; + } + } + + if (statRestored) { + globalScene.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 class PokemonExpBoosterModifier extends PokemonHeldItemModifier { + public override type: PokemonExpBoosterModifierType; + + private boostMultiplier: number; + + constructor(type: PokemonExpBoosterModifierType, pokemonId: number, boostPercent: number, stackCount?: number) { + super(type, pokemonId, stackCount); + this.boostMultiplier = boostPercent * 0.01; + } + + matchType(modifier: Modifier): boolean { + if (modifier instanceof PokemonExpBoosterModifier) { + const pokemonExpModifier = modifier as PokemonExpBoosterModifier; + return pokemonExpModifier.boostMultiplier === this.boostMultiplier; + } + return false; + } + + clone(): PersistentModifier { + return new PokemonExpBoosterModifier(this.type, this.pokemonId, this.boostMultiplier * 100, this.stackCount); + } + + getArgs(): any[] { + return super.getArgs().concat(this.boostMultiplier * 100); + } + + /** + * Checks if {@linkcode PokemonExpBoosterModifier} should be applied + * @param pokemon The {@linkcode Pokemon} to apply the exp boost to + * @param boost {@linkcode NumberHolder} holding the exp boost value + * @returns `true` if {@linkcode PokemonExpBoosterModifier} should be applied + */ + override shouldApply(pokemon: Pokemon, boost: NumberHolder): boolean { + return super.shouldApply(pokemon, boost) && !!boost; + } + + /** + * Applies {@linkcode PokemonExpBoosterModifier} + * @param _pokemon The {@linkcode Pokemon} to apply the exp boost to + * @param boost {@linkcode NumberHolder} holding the exp boost value + * @returns always `true` + */ + override apply(_pokemon: Pokemon, boost: NumberHolder): boolean { + boost.value = Math.floor(boost.value * (1 + this.getStackCount() * this.boostMultiplier)); + + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + return 99; + } +} + +export class PokemonFriendshipBoosterModifier extends PokemonHeldItemModifier { + public override type: PokemonFriendshipBoosterModifierType; + + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonFriendshipBoosterModifier; + } + + clone(): PersistentModifier { + return new PokemonFriendshipBoosterModifier(this.type, this.pokemonId, this.stackCount); + } + + /** + * Applies {@linkcode PokemonFriendshipBoosterModifier} + * @param _pokemon The {@linkcode Pokemon} to apply the friendship boost to + * @param friendship {@linkcode NumberHolder} holding the friendship boost value + * @returns always `true` + */ + override apply(_pokemon: Pokemon, friendship: NumberHolder): boolean { + friendship.value = Math.floor(friendship.value * (1 + 0.5 * this.getStackCount())); + + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + return 3; + } +} + +export class PokemonNatureWeightModifier extends PokemonHeldItemModifier { + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonNatureWeightModifier; + } + + clone(): PersistentModifier { + return new PokemonNatureWeightModifier(this.type, this.pokemonId, this.stackCount); + } + + /** + * Applies {@linkcode PokemonNatureWeightModifier} + * @param _pokemon The {@linkcode Pokemon} to apply the nature weight to + * @param multiplier {@linkcode NumberHolder} holding the nature weight + * @returns `true` if multiplier was applied + */ + override apply(_pokemon: Pokemon, multiplier: NumberHolder): boolean { + if (multiplier.value !== 1) { + multiplier.value += 0.1 * this.getStackCount() * (multiplier.value > 1 ? 1 : -1); + return true; + } + + return false; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + return 10; + } +} + +export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier { + public override type: PokemonMoveAccuracyBoosterModifierType; + private accuracyAmount: number; + + constructor(type: PokemonMoveAccuracyBoosterModifierType, pokemonId: number, accuracy: number, stackCount?: number) { + super(type, pokemonId, stackCount); + this.accuracyAmount = accuracy; + } + + matchType(modifier: Modifier): boolean { + if (modifier instanceof PokemonMoveAccuracyBoosterModifier) { + const pokemonAccuracyBoosterModifier = modifier as PokemonMoveAccuracyBoosterModifier; + return pokemonAccuracyBoosterModifier.accuracyAmount === this.accuracyAmount; + } + return false; + } + + clone(): PersistentModifier { + return new PokemonMoveAccuracyBoosterModifier(this.type, this.pokemonId, this.accuracyAmount, this.stackCount); + } + + getArgs(): any[] { + return super.getArgs().concat(this.accuracyAmount); + } + + /** + * Checks if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied + * @param pokemon The {@linkcode Pokemon} to apply the move accuracy boost to + * @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost + * @returns `true` if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied + */ + override shouldApply(pokemon?: Pokemon, moveAccuracy?: NumberHolder): boolean { + return super.shouldApply(pokemon, moveAccuracy) && !!moveAccuracy; + } + + /** + * Applies {@linkcode PokemonMoveAccuracyBoosterModifier} + * @param _pokemon The {@linkcode Pokemon} to apply the move accuracy boost to + * @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost + * @returns always `true` + */ + override apply(_pokemon: Pokemon, moveAccuracy: NumberHolder): boolean { + moveAccuracy.value = moveAccuracy.value + this.accuracyAmount * this.getStackCount(); + + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + return 3; + } +} + +export class PokemonMultiHitModifier extends PokemonHeldItemModifier { + public override type: PokemonMultiHitModifierType; + + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonMultiHitModifier; + } + + clone(): PersistentModifier { + return new PokemonMultiHitModifier(this.type, this.pokemonId, this.stackCount); + } + + /** + * For each stack, converts 25 percent of attack damage into an additional strike. + * @param pokemon The {@linkcode Pokemon} using the move + * @param moveId The {@linkcode Moves | 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: Moves, + count: NumberHolder | null = null, + damageMultiplier: NumberHolder | null = null, + ): boolean { + const move = allMoves[moveId]; + /** + * The move must meet Parental Bond's restrictions for this item + * to apply. This means + * - Only attacks are boosted + * - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted + * (though Multi-Lens can still affect moves boosted by Parental Bond) + * - Multi-target moves are not boosted *unless* they can only hit a single Pokemon + * - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted + */ + if (!move.canBeMultiStrikeEnhanced(pokemon)) { + return false; + } + + if (!isNullOrUndefined(count)) { + return this.applyHitCountBoost(count); + } + if (!isNullOrUndefined(damageMultiplier)) { + return this.applyDamageModifier(pokemon, damageMultiplier); + } + + return false; + } + + /** Adds strikes to a move equal to the number of stacked Multi-Lenses */ + private applyHitCountBoost(count: NumberHolder): boolean { + count.value += this.getStackCount(); + return true; + } + + /** + * If applied to the first hit of a move, sets the damage multiplier + * equal to (1 - the number of stacked Multi-Lenses). + * Additional strikes beyond that are given a 0.25x damage multiplier + */ + private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { + if (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) { + // Reduce first hit by 25% for each stack count + damageMultiplier.value *= 1 - 0.25 * this.getStackCount(); + return true; + } + if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== this.getStackCount() + 1) { + // Deal 25% damage for each remaining Multi Lens hit + damageMultiplier.value *= 0.25; + return true; + } + // An extra hit not caused by Multi Lens -- assume it is Parental Bond + return false; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + return 2; + } +} + +export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier { + public override type: FormChangeItemModifierType; + public formChangeItem: FormChangeItem; + public active: boolean; + public isTransferable = false; + + constructor( + type: FormChangeItemModifierType, + pokemonId: number, + formChangeItem: FormChangeItem, + active: boolean, + stackCount?: number, + ) { + super(type, pokemonId, stackCount); + this.formChangeItem = formChangeItem; + this.active = active; + } + + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonFormChangeItemModifier && modifier.formChangeItem === this.formChangeItem; + } + + clone(): PersistentModifier { + return new PokemonFormChangeItemModifier( + this.type, + this.pokemonId, + this.formChangeItem, + this.active, + this.stackCount, + ); + } + + getArgs(): any[] { + return super.getArgs().concat(this.formChangeItem, this.active); + } + + /** + * Applies {@linkcode PokemonFormChangeItemModifier} + * @param pokemon The {@linkcode Pokemon} to apply the form change item to + * @param active `true` if the form change item is active + * @returns `true` if the form change item was applied + */ + override apply(pokemon: Pokemon, active: boolean): boolean { + const switchActive = this.active && !active; + + if (switchActive) { + this.active = false; + } + + const ret = globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger); + + if (switchActive) { + this.active = true; + } + + return ret; + } + + getMaxHeldItemCount(_pokemon: Pokemon): number { + return 1; + } +} + +export class 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 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 instanceof Pokemon ? pokemon.getOpponents() : []; + } + + /** + * Steals an item from a set of target Pokemon. + * This prioritizes high-tier held items when selecting the item to steal. + * @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 poolType = pokemon.isPlayer() + ? ModifierPoolType.PLAYER + : pokemon.hasTrainer() + ? ModifierPoolType.TRAINER + : ModifierPoolType.WILD; + + const transferredModifierTypes: ModifierType[] = []; + const itemModifiers = globalScene.findModifiers( + m => m instanceof PokemonHeldItemModifier && m.pokemonId === targetPokemon.id && m.isTransferable, + targetPokemon.isPlayer(), + ) as PokemonHeldItemModifier[]; + let highestItemTier = itemModifiers + .map(m => getOrInferTier(m.type, poolType)) + .reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct? + let tierItemModifiers = itemModifiers.filter(m => getOrInferTier(m.type, poolType) === highestItemTier); + + for (let i = 0; i < transferredItemCount; i++) { + if (!tierItemModifiers.length) { + while (highestItemTier-- && !tierItemModifiers.length) { + tierItemModifiers = itemModifiers.filter(m => m.type.tier === highestItemTier); + } + if (!tierItemModifiers.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.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 Phaser.Math.RND.realInRange(0, 1) < 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; + } +} diff --git a/src/modifier/modifier-bar.ts b/src/modifier/modifier-bar.ts new file mode 100644 index 00000000000..2c55312667a --- /dev/null +++ b/src/modifier/modifier-bar.ts @@ -0,0 +1,106 @@ +import { globalScene } from "#app/global-scene"; +import { PokemonHeldItemModifier } from "./held-item-modifier"; +import type { Modifier, PersistentModifier } from "./modifier"; + +const iconOverflowIndex = 24; + +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 const modifierSortFunc = (a: Modifier, b: Modifier): number => { + const itemNameMatch = a.type.name.localeCompare(b.type.name); + const typeNameMatch = a.constructor.name.localeCompare(b.constructor.name); + const aId = a instanceof PokemonHeldItemModifier && a.pokemonId ? a.pokemonId : 4294967295; + const bId = b instanceof PokemonHeldItemModifier && b.pokemonId ? b.pokemonId : 4294967295; + + //First sort by pokemonID + if (aId < bId) { + return 1; + } + if (aId > bId) { + return -1; + } + if (aId === bId) { + //Then sort by item type + if (typeNameMatch === 0) { + return itemNameMatch; + //Finally sort by item name + } + return typeNameMatch; + } + return 0; +}; diff --git a/src/modifier/modifier-pool-type.ts b/src/modifier/modifier-pool-type.ts new file mode 100644 index 00000000000..ca3ed3a11c7 --- /dev/null +++ b/src/modifier/modifier-pool-type.ts @@ -0,0 +1,7 @@ +export enum ModifierPoolType { + PLAYER, + WILD, + TRAINER, + ENEMY_BUFF, + DAILY_STARTER, +} diff --git a/src/modifier/modifier-pool.ts b/src/modifier/modifier-pool.ts new file mode 100644 index 00000000000..1674a88cd6e --- /dev/null +++ b/src/modifier/modifier-pool.ts @@ -0,0 +1,1468 @@ +import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import type Pokemon from "#app/field/pokemon"; +import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { timedEventManager } from "#app/global-event-manager"; +import { globalScene } from "#app/global-scene"; +import { Unlockables } from "#app/system/unlockables"; +import { getEnumValues, isNullOrUndefined, randSeedInt } from "#app/utils/common"; +import { Abilities } from "#enums/abilities"; +import { BerryType } from "#enums/berry-type"; +import { Moves } from "#enums/moves"; +import { PokeballType } from "#enums/pokeball"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import { DoubleBattleChanceBoosterModifier, type EnemyPersistentModifier, type PersistentModifier } from "./modifier"; +import { + BerryModifier, + type PokemonHeldItemModifier, + SpeciesCritBoosterModifier, + TurnStatusEffectModifier, +} from "./held-item-modifier"; +import { ModifierTier } from "./modifier-tier"; +import { + FormChangeItemModifierType, + getModifierType, + type ModifierOverride, + type ModifierType, + type ModifierTypeFunc, + ModifierTypeGenerator, + type ModifierTypeKeys, + ModifierTypeOption, + modifierTypes, + PokemonHeldItemModifierType, +} from "./modifier-type"; +import { getPartyLuckValue, hasMaximumBalls } from "./modifier-utils"; +import Overrides from "#app/overrides"; +import { ModifierPoolType } from "./modifier-pool-type"; + +const outputModifierData = false; +const useMaxWeightForOutput = false; + +interface ModifierPool { + [tier: string]: WeightedModifierType[]; +} + +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(modifierTypes).find(k => modifierTypes[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 WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number; + +/** + * 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 + */ +export 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 + */ +export 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 + */ +export 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; + }; +} + +const modifierPool: 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; + }), + [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(Species.TERAPAGOS) || p.hasSpecies(Species.OGERPON) || p.hasSpecies(Species.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; + }), + [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 = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.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 = [Moves.FACADE, Moves.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 = [ + /* Moves.TRICK, Moves.FLING, Moves.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 = [ + Abilities.QUICK_FEET, + Abilities.GUTS, + Abilities.MARVEL_SCALE, + Abilities.MAGIC_GUARD, + ].some(a => p.hasAbility(a, false, true)); + const hasSpecificAbility = [Abilities.TOXIC_BOOST, Abilities.POISON_HEAL].some(a => + p.hasAbility(a, false, true), + ); + const hasOppositeAbility = [Abilities.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 = [Moves.FACADE, Moves.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 = [ + /* Moves.TRICK, Moves.FLING, Moves.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 = [ + Abilities.QUICK_FEET, + Abilities.GUTS, + Abilities.MARVEL_SCALE, + Abilities.MAGIC_GUARD, + ].some(a => p.hasAbility(a, false, true)); + const hasSpecificAbility = [Abilities.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); + const hasOppositeAbility = [Abilities.TOXIC_BOOST, Abilities.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 = [ + Abilities.DROUGHT, + Abilities.ORICHALCUM_PULSE, + Abilities.DRIZZLE, + Abilities.SAND_STREAM, + Abilities.SAND_SPIT, + Abilities.SNOW_WARNING, + Abilities.ELECTRIC_SURGE, + Abilities.HADRON_ENGINE, + Abilities.PSYCHIC_SURGE, + Abilities.GRASSY_SURGE, + Abilities.SEED_SOWER, + Abilities.MISTY_SURGE, + ].some(a => p.hasAbility(a, false, true)); + + const hasMoves = [ + Moves.SUNNY_DAY, + Moves.RAIN_DANCE, + Moves.SANDSTORM, + Moves.SNOWSCAPE, + Moves.HAIL, + Moves.CHILLY_RECEPTION, + Moves.ELECTRIC_TERRAIN, + Moves.PSYCHIC_TERRAIN, + Moves.GRASSY_TERRAIN, + Moves.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; + }), + [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; + }), + [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; + }), +}; + +const wildModifierPool: ModifierPool = { + [ModifierTier.COMMON]: [new WeightedModifierType(modifierTypes.BERRY, 1)].map(m => { + m.setTier(ModifierTier.COMMON); + return m; + }), + [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1)].map(m => { + m.setTier(ModifierTier.GREAT); + return m; + }), + [ModifierTier.ULTRA]: [ + new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), + new WeightedModifierType(modifierTypes.WHITE_HERB, 0), + ].map(m => { + m.setTier(ModifierTier.ULTRA); + return m; + }), + [ModifierTier.ROGUE]: [new WeightedModifierType(modifierTypes.LUCKY_EGG, 4)].map(m => { + m.setTier(ModifierTier.ROGUE); + return m; + }), + [ModifierTier.MASTER]: [new WeightedModifierType(modifierTypes.GOLDEN_EGG, 1)].map(m => { + m.setTier(ModifierTier.MASTER); + return m; + }), +}; + +const trainerModifierPool: ModifierPool = { + [ModifierTier.COMMON]: [ + new WeightedModifierType(modifierTypes.BERRY, 8), + new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), + ].map(m => { + m.setTier(ModifierTier.COMMON); + return m; + }), + [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3)].map(m => { + m.setTier(ModifierTier.GREAT); + return m; + }), + [ModifierTier.ULTRA]: [ + new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), + new WeightedModifierType(modifierTypes.WHITE_HERB, 0), + ].map(m => { + m.setTier(ModifierTier.ULTRA); + return m; + }), + [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; + }), + [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; + }), +}; + +const enemyBuffModifierPool: ModifierPool = { + [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; + }), + [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; + }), + [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; + }), + [ModifierTier.ROGUE]: [].map((m: WeightedModifierType) => { + m.setTier(ModifierTier.ROGUE); + return m; + }), + [ModifierTier.MASTER]: [].map((m: WeightedModifierType) => { + m.setTier(ModifierTier.MASTER); + return m; + }), +}; + +const dailyStarterModifierPool: ModifierPool = { + [ModifierTier.COMMON]: [ + new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1), + new WeightedModifierType(modifierTypes.BERRY, 3), + ].map(m => { + m.setTier(ModifierTier.COMMON); + return m; + }), + [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 5)].map(m => { + m.setTier(ModifierTier.GREAT); + return m; + }), + [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; + }), + [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; + }), + [ModifierTier.MASTER]: [ + new WeightedModifierType(modifierTypes.LEFTOVERS, 1), + new WeightedModifierType(modifierTypes.SHELL_BELL, 1), + ].map(m => { + m.setTier(ModifierTier.MASTER); + return m; + }), +}; + +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 = {}; + +export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool { + let pool: ModifierPool; + switch (poolType) { + case ModifierPoolType.PLAYER: + pool = modifierPool; + break; + case ModifierPoolType.WILD: + pool = wildModifierPool; + break; + case ModifierPoolType.TRAINER: + pool = trainerModifierPool; + break; + case ModifierPoolType.ENEMY_BUFF: + pool = enemyBuffModifierPool; + break; + case ModifierPoolType.DAILY_STARTER: + pool = dailyStarterModifierPool; + break; + } + return pool; +} + +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 { + guaranteedModifierTiers?: ModifierTier[]; + guaranteedModifierTypeOptions?: ModifierTypeOption[]; + guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; + fillRemaining?: boolean; + /** Set to negative value to disable rerolls completely in shop */ + rerollMultiplier?: number; + allowLuckUpgrades?: boolean; +} + +/** + * 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 (Optional) If specified, can customize the item shop rewards further. + * - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` If specified, will override the first X items to be specific modifier options (these should be pre-genned). + * - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). + * - `guaranteedModifierTiers?: ModifierTier[]` If specified, will override the next X items to be the specified tier. These can upgrade with luck. + * - `fillRemaining?: boolean` Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value. + * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`, + * - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally. + * - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value). + * - `rerollMultiplier?: number` If specified, can adjust the amount of money required for a shop reroll. If set to a negative value, the shop will not allow rerolls at all. + * - `allowLuckUpgrades?: boolean` Default `true`, if `false` will prevent set item tiers from upgrading via luck + */ +export function getPlayerModifierTypeOptions( + count: number, + party: PlayerPokemon[], + modifierTiers?: ModifierTier[], + customModifierSettings?: CustomModifierSettings, +): ModifierTypeOption[] { + const options: ModifierTypeOption[] = []; + const retryCount = Math.min(count * 5, 50); + if (!customModifierSettings) { + new Array(count).fill(0).map((_, i) => { + options.push( + getModifierTypeOptionWithRetry( + options, + retryCount, + party, + modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined, + ), + ); + }); + } 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(modifierTypes).find(k => modifierTypes[k] === mod) as string; + let guaranteedMod: ModifierType = modifierTypes[modifierId]?.(); + + // Populates item id and tier + guaranteedMod = withTierFromPool( + guaranteedMod.withIdFromFunc(modifierTypes[modifierId]), + 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 = modifierTypes[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 = withTierFromPool(modifierType.withIdFromFunc(modifierFunc), ModifierPoolType.PLAYER, party); + } + } +} + +export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseCost: number): ModifierTypeOption[] { + if (!(waveIndex % 10)) { + return []; + } + + const options = [ + [ + new ModifierTypeOption(modifierTypes.POTION(), 0, baseCost * 0.2), + new ModifierTypeOption(modifierTypes.ETHER(), 0, baseCost * 0.4), + new ModifierTypeOption(modifierTypes.REVIVE(), 0, baseCost * 2), + ], + [ + new ModifierTypeOption(modifierTypes.SUPER_POTION(), 0, baseCost * 0.45), + new ModifierTypeOption(modifierTypes.FULL_HEAL(), 0, baseCost), + ], + [ + new ModifierTypeOption(modifierTypes.ELIXIR(), 0, baseCost), + new ModifierTypeOption(modifierTypes.MAX_ETHER(), 0, baseCost), + ], + [ + new ModifierTypeOption(modifierTypes.HYPER_POTION(), 0, baseCost * 0.8), + new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75), + new ModifierTypeOption(modifierTypes.MEMORY_MUSHROOM(), 0, baseCost * 4), + ], + [ + new ModifierTypeOption(modifierTypes.MAX_POTION(), 0, baseCost * 1.5), + new ModifierTypeOption(modifierTypes.MAX_ELIXIR(), 0, baseCost * 2.5), + ], + [new ModifierTypeOption(modifierTypes.FULL_RESTORE(), 0, baseCost * 2.25)], + [new ModifierTypeOption(modifierTypes.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(modifierTypes.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 && (!modifierPool.hasOwnProperty(tier) || !modifierPool[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 (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[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 { + let modifierType: ModifierType | WeightedModifierType = modifierPool[tier || ModifierTier.COMMON][0]; + if (modifierType instanceof WeightedModifierType) { + modifierType = (modifierType as WeightedModifierType).modifierType; + } + return modifierType; +} + +export function getOrInferTier( + modifierType: ModifierType, + poolType: ModifierPoolType = ModifierPoolType.PLAYER, +): ModifierTier | null { + if (modifierType.tier) { + return modifierType.tier; + } + if (!modifierType.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 === modifierType.id)) { + return (modifierType.tier = tier); + } + } + } + return null; +} + +/** + * 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}) + */ +export function withTierFromPool( + modifierType: ModifierType, + 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 (modifierType.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) { + modifierType.tier = modifier.modifierType.tier; + return modifierType; + } + 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) { + modifierType.tier = defaultTier; + } + + return modifierType; +} diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 912e12f19dc..34bf2c43eb1 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -5,7 +5,7 @@ import { getBerryEffectDescription, getBerryName } from "#app/data/berry"; import { AttackMove } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; -import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; +import { getPokeballCatchMultiplier, getPokeballName } from "#app/data/pokeball"; import { FormChangeItem, pokemonFormChanges, @@ -14,20 +14,13 @@ import { } from "#app/data/pokemon-forms"; import { getStatusEffectDescriptor } from "#app/data/status-effect"; import { PokemonType } from "#enums/pokemon-type"; -import type { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import type { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { AddPokeballModifier, AddVoucherModifier, - AttackTypeBoosterModifier, - BaseStatModifier, - BerryModifier, BoostBugSpawnModifier, - BypassSpeedChanceModifier, - ContactHeldItemTransferChanceModifier, - CritBoosterModifier, - DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, @@ -37,19 +30,15 @@ import { EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, - EvolutionStatBoosterModifier, - EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, - FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, - HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, @@ -60,63 +49,64 @@ import { MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, + PokemonHpRestoreModifier, + PokemonLevelIncrementModifier, + PokemonNatureChangeModifier, + PokemonPpRestoreModifier, + PokemonPpUpModifier, + PokemonStatusHealModifier, + RememberMoveModifier, + ShinyRateBoosterModifier, + TempCritBoosterModifier, + TempStatStageBoosterModifier, + TerastallizeAccessModifier, + TerrastalizeModifier, + TmModifier, + type Modifier, + TempExtraModifierModifier, + CriticalCatchChanceBoosterModifier, + PreserveBerryModifier, +} from "#app/modifier/modifier"; +import { + AttackTypeBoosterModifier, + BaseStatModifier, + BerryModifier, + BypassSpeedChanceModifier, + ContactHeldItemTransferChanceModifier, + CritBoosterModifier, + DamageMoneyRewardModifier, + EvolutionStatBoosterModifier, + EvoTrackerModifier, + FlinchChanceModifier, + HitHealModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, - PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, - PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, - PokemonNatureChangeModifier, PokemonNatureWeightModifier, - PokemonPpRestoreModifier, - PokemonPpUpModifier, - PokemonStatusHealModifier, - PreserveBerryModifier, - RememberMoveModifier, ResetNegativeStatStageModifier, - ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, - TempCritBoosterModifier, - TempStatStageBoosterModifier, - TerastallizeAccessModifier, - TerrastalizeModifier, - TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, - type EnemyPersistentModifier, - type Modifier, - type PersistentModifier, - TempExtraModifierModifier, - CriticalCatchChanceBoosterModifier, FieldEffectModifier, -} from "#app/modifier/modifier"; +} from "#app/modifier/held-item-modifier"; import { ModifierTier } from "#app/modifier/modifier-tier"; -import Overrides from "#app/overrides"; -import { Unlockables } from "#app/system/unlockables"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import PartyUiHandler from "#app/ui/party-ui-handler"; -import { getModifierTierTextTint } from "#app/ui/text"; -import { - formatMoney, - getEnumKeys, - getEnumValues, - isNullOrUndefined, - NumberHolder, - padInt, - randSeedInt, -} from "#app/utils/common"; -import { Abilities } from "#enums/abilities"; +import Overrides from "#app/overrides"; + +import { formatMoney, getEnumKeys, getEnumValues, NumberHolder, padInt, randSeedInt } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; @@ -128,20 +118,8 @@ import type { PermanentStat, TempBattleStat } from "#enums/stat"; import { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; -import { timedEventManager } from "#app/global-event-manager"; import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; -const outputModifierData = false; -const useMaxWeightForOutput = false; - -export enum ModifierPoolType { - PLAYER, - WILD, - TRAINER, - ENEMY_BUFF, - DAILY_STARTER, -} - type NewModifierFunc = (type: ModifierType, args: any[]) => Modifier; export class ModifierType { @@ -179,43 +157,6 @@ export class ModifierType { 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 @@ -225,53 +166,6 @@ export class ModifierType { 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); @@ -1796,70 +1690,6 @@ export class EnemyEndureChanceModifierType extends ModifierType { } export type ModifierTypeFunc = () => ModifierType; -type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number; - -/** - * 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; - }; -} -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(modifierTypes).find(k => modifierTypes[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; @@ -2422,738 +2252,6 @@ export const modifierTypes = { ), }; -interface ModifierPool { - [tier: string]: WeightedModifierType[]; -} - -/** - * 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; -} - -const modifierPool: 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; - }), - [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(Species.TERAPAGOS) || p.hasSpecies(Species.OGERPON) || p.hasSpecies(Species.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; - }), - [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 = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.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 = [Moves.FACADE, Moves.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 = [ - /* Moves.TRICK, Moves.FLING, Moves.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 = [ - Abilities.QUICK_FEET, - Abilities.GUTS, - Abilities.MARVEL_SCALE, - Abilities.MAGIC_GUARD, - ].some(a => p.hasAbility(a, false, true)); - const hasSpecificAbility = [Abilities.TOXIC_BOOST, Abilities.POISON_HEAL].some(a => - p.hasAbility(a, false, true), - ); - const hasOppositeAbility = [Abilities.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 = [Moves.FACADE, Moves.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 = [ - /* Moves.TRICK, Moves.FLING, Moves.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 = [ - Abilities.QUICK_FEET, - Abilities.GUTS, - Abilities.MARVEL_SCALE, - Abilities.MAGIC_GUARD, - ].some(a => p.hasAbility(a, false, true)); - const hasSpecificAbility = [Abilities.FLARE_BOOST].some(a => p.hasAbility(a, false, true)); - const hasOppositeAbility = [Abilities.TOXIC_BOOST, Abilities.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 = [ - Abilities.DROUGHT, - Abilities.ORICHALCUM_PULSE, - Abilities.DRIZZLE, - Abilities.SAND_STREAM, - Abilities.SAND_SPIT, - Abilities.SNOW_WARNING, - Abilities.ELECTRIC_SURGE, - Abilities.HADRON_ENGINE, - Abilities.PSYCHIC_SURGE, - Abilities.GRASSY_SURGE, - Abilities.SEED_SOWER, - Abilities.MISTY_SURGE, - ].some(a => p.hasAbility(a, false, true)); - - const hasMoves = [ - Moves.SUNNY_DAY, - Moves.RAIN_DANCE, - Moves.SANDSTORM, - Moves.SNOWSCAPE, - Moves.HAIL, - Moves.CHILLY_RECEPTION, - Moves.ELECTRIC_TERRAIN, - Moves.PSYCHIC_TERRAIN, - Moves.GRASSY_TERRAIN, - Moves.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; - }), - [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; - }), - [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; - }), -}; - -const wildModifierPool: ModifierPool = { - [ModifierTier.COMMON]: [new WeightedModifierType(modifierTypes.BERRY, 1)].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }), - [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }), - [ModifierTier.ULTRA]: [ - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.WHITE_HERB, 0), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }), - [ModifierTier.ROGUE]: [new WeightedModifierType(modifierTypes.LUCKY_EGG, 4)].map(m => { - m.setTier(ModifierTier.ROGUE); - return m; - }), - [ModifierTier.MASTER]: [new WeightedModifierType(modifierTypes.GOLDEN_EGG, 1)].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }), -}; - -const trainerModifierPool: ModifierPool = { - [ModifierTier.COMMON]: [ - new WeightedModifierType(modifierTypes.BERRY, 8), - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }), - [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }), - [ModifierTier.ULTRA]: [ - new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10), - new WeightedModifierType(modifierTypes.WHITE_HERB, 0), - ].map(m => { - m.setTier(ModifierTier.ULTRA); - return m; - }), - [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; - }), - [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; - }), -}; - -const enemyBuffModifierPool: ModifierPool = { - [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; - }), - [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; - }), - [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; - }), - [ModifierTier.ROGUE]: [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.ROGUE); - return m; - }), - [ModifierTier.MASTER]: [].map((m: WeightedModifierType) => { - m.setTier(ModifierTier.MASTER); - return m; - }), -}; - -const dailyStarterModifierPool: ModifierPool = { - [ModifierTier.COMMON]: [ - new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1), - new WeightedModifierType(modifierTypes.BERRY, 3), - ].map(m => { - m.setTier(ModifierTier.COMMON); - return m; - }), - [ModifierTier.GREAT]: [new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 5)].map(m => { - m.setTier(ModifierTier.GREAT); - return m; - }), - [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; - }), - [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; - }), - [ModifierTier.MASTER]: [ - new WeightedModifierType(modifierTypes.LEFTOVERS, 1), - new WeightedModifierType(modifierTypes.SHELL_BELL, 1), - ].map(m => { - m.setTier(ModifierTier.MASTER); - return m; - }), -}; - export function getModifierType(modifierTypeFunc: ModifierTypeFunc): ModifierType { const modifierType = modifierTypeFunc(); if (!modifierType.id) { @@ -3162,560 +2260,6 @@ export function getModifierType(modifierTypeFunc: ModifierTypeFunc): ModifierTyp return modifierType; } -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 = {}; - -export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool { - let pool: ModifierPool; - switch (poolType) { - case ModifierPoolType.PLAYER: - pool = modifierPool; - break; - case ModifierPoolType.WILD: - pool = wildModifierPool; - break; - case ModifierPoolType.TRAINER: - pool = trainerModifierPool; - break; - case ModifierPoolType.ENEMY_BUFF: - pool = enemyBuffModifierPool; - break; - case ModifierPoolType.DAILY_STARTER: - pool = dailyStarterModifierPool; - break; - } - return pool; -} - -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 { - guaranteedModifierTiers?: ModifierTier[]; - guaranteedModifierTypeOptions?: ModifierTypeOption[]; - guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; - fillRemaining?: boolean; - /** Set to negative value to disable rerolls completely in shop */ - rerollMultiplier?: number; - allowLuckUpgrades?: boolean; -} - -export function getModifierTypeFuncById(id: string): ModifierTypeFunc { - return modifierTypes[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 (Optional) If specified, can customize the item shop rewards further. - * - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` If specified, will override the first X items to be specific modifier options (these should be pre-genned). - * - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). - * - `guaranteedModifierTiers?: ModifierTier[]` If specified, will override the next X items to be the specified tier. These can upgrade with luck. - * - `fillRemaining?: boolean` Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value. - * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`, - * - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally. - * - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value). - * - `rerollMultiplier?: number` If specified, can adjust the amount of money required for a shop reroll. If set to a negative value, the shop will not allow rerolls at all. - * - `allowLuckUpgrades?: boolean` Default `true`, if `false` will prevent set item tiers from upgrading via luck - */ -export function getPlayerModifierTypeOptions( - count: number, - party: PlayerPokemon[], - modifierTiers?: ModifierTier[], - customModifierSettings?: CustomModifierSettings, -): ModifierTypeOption[] { - const options: ModifierTypeOption[] = []; - const retryCount = Math.min(count * 5, 50); - if (!customModifierSettings) { - new Array(count).fill(0).map((_, i) => { - options.push( - getModifierTypeOptionWithRetry( - options, - retryCount, - party, - modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined, - ), - ); - }); - } 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(modifierTypes).find(k => modifierTypes[k] === mod) as string; - let guaranteedMod: ModifierType = modifierTypes[modifierId]?.(); - - // Populates item id and tier - guaranteedMod = guaranteedMod - .withIdFromFunc(modifierTypes[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 = modifierTypes[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(modifierTypes.POTION(), 0, baseCost * 0.2), - new ModifierTypeOption(modifierTypes.ETHER(), 0, baseCost * 0.4), - new ModifierTypeOption(modifierTypes.REVIVE(), 0, baseCost * 2), - ], - [ - new ModifierTypeOption(modifierTypes.SUPER_POTION(), 0, baseCost * 0.45), - new ModifierTypeOption(modifierTypes.FULL_HEAL(), 0, baseCost), - ], - [ - new ModifierTypeOption(modifierTypes.ELIXIR(), 0, baseCost), - new ModifierTypeOption(modifierTypes.MAX_ETHER(), 0, baseCost), - ], - [ - new ModifierTypeOption(modifierTypes.HYPER_POTION(), 0, baseCost * 0.8), - new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75), - new ModifierTypeOption(modifierTypes.MEMORY_MUSHROOM(), 0, baseCost * 4), - ], - [ - new ModifierTypeOption(modifierTypes.MAX_POTION(), 0, baseCost * 1.5), - new ModifierTypeOption(modifierTypes.MAX_ELIXIR(), 0, baseCost * 2.5), - ], - [new ModifierTypeOption(modifierTypes.FULL_RESTORE(), 0, baseCost * 2.25)], - [new ModifierTypeOption(modifierTypes.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(modifierTypes.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 && (!modifierPool.hasOwnProperty(tier) || !modifierPool[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 (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[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 { - 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; @@ -3728,52 +2272,49 @@ export class ModifierTypeOption { } } +export function getModifierTypeFuncById(id: string): ModifierTypeFunc { + return modifierTypes[id]; +} + /** - * 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. + * 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 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; +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; } - 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; + 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 instanceof 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); + } + } } - return getModifierTierTextTint(modifierTier); } diff --git a/src/modifier/modifier-utils.ts b/src/modifier/modifier-utils.ts new file mode 100644 index 00000000000..60e0be19802 --- /dev/null +++ b/src/modifier/modifier-utils.ts @@ -0,0 +1,67 @@ +import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; +import type Pokemon from "#app/field/pokemon"; +import { timedEventManager } from "#app/global-event-manager"; +import { globalScene } from "#app/global-scene"; +import { getModifierTierTextTint } from "#app/ui/text"; +import { NumberHolder, randSeedInt } from "#app/utils/common"; +import type { PokeballType } from "#enums/pokeball"; +import { ModifierTier } from "./modifier-tier"; + +/** + * 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 + */ +export function hasMaximumBalls(ballType: PokeballType): boolean { + return globalScene.gameMode.isClassic && globalScene.pokeballCounts[ballType] >= MAX_PER_TYPE_POKEBALLS; +} + +/** + * 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); +} diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 42e0155bdd8..e9dc87c939d 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1,11 +1,7 @@ import { FusionSpeciesFormEvolution, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; -import { getBerryEffectFunc, getBerryPredicate } from "#app/data/berry"; import { getLevelTotalExp } from "#app/data/exp"; -import { allMoves } from "#app/data/data-lists"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; -import { type FormChangeItem, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectHealText } from "#app/data/status-effect"; -import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { EvolutionPhase } from "#app/phases/evolution-phase"; @@ -13,153 +9,35 @@ import { LearnMovePhase, LearnMoveType } from "#app/phases/learn-move-phase"; import { LevelUpPhase } from "#app/phases/level-up-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import type { VoucherType } from "#app/system/voucher"; -import { Command } from "#app/ui/command-ui-handler"; import { addTextObject, TextStyle } from "#app/ui/text"; -import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { BerryType } from "#enums/berry-type"; -import type { Moves } from "#enums/moves"; import type { Nature } from "#enums/nature"; import type { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; -import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; +import { type TempBattleStat, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import type { PokemonType } from "#enums/pokemon-type"; import i18next from "i18next"; import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, - type FormChangeItemModifierType, type ModifierOverride, type ModifierType, - type PokemonBaseStatTotalModifierType, - type PokemonExpBoosterModifierType, - type PokemonFriendshipBoosterModifierType, - type PokemonMoveAccuracyBoosterModifierType, - type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, - ModifierPoolType, ModifierTypeGenerator, modifierTypes, - PokemonHeldItemModifierType, } from "./modifier-type"; -import { Color, ShadowColor } from "#enums/color"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; -import { - applyAbAttrs, - applyPostItemLostAbAttrs, - CommanderAbAttr, - PostItemLostAbAttr, -} from "#app/data/abilities/ability"; import { globalScene } from "#app/global-scene"; +import type { EvoTrackerModifier } from "./held-item-modifier"; // TODO: Get rid of this circular import +import { type BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils/common"; +import type Pokemon from "#app/field/pokemon"; +import type { PlayerPokemon } from "#app/field/pokemon"; export type ModifierPredicate = (modifier: Modifier) => boolean; -const iconOverflowIndex = 24; - -export const modifierSortFunc = (a: Modifier, b: Modifier): number => { - const itemNameMatch = a.type.name.localeCompare(b.type.name); - const typeNameMatch = a.constructor.name.localeCompare(b.constructor.name); - const aId = a instanceof PokemonHeldItemModifier && a.pokemonId ? a.pokemonId : 4294967295; - const bId = b instanceof PokemonHeldItemModifier && b.pokemonId ? b.pokemonId : 4294967295; - - //First sort by pokemonID - if (aId < bId) { - return 1; - } - if (aId > bId) { - return -1; - } - if (aId === bId) { - //Then sort by item type - if (typeNameMatch === 0) { - return itemNameMatch; - //Finally sort by item name - } - return typeNameMatch; - } - return 0; -}; - -export class ModifierBar extends Phaser.GameObjects.Container { - private player: boolean; - private modifierCache: PersistentModifier[]; - - constructor(enemy?: boolean) { - super(globalScene, 1 + (enemy ? 302 : 0), 2); - - this.player = !enemy; - this.setScale(0.5); - } - - /** - * Method to update content displayed in {@linkcode ModifierBar} - * @param {PersistentModifier[]} modifiers - The list of modifiers to be displayed in the {@linkcode ModifierBar} - * @param {boolean} hideHeldItems - If set to "true", only modifiers not assigned to a Pokémon are displayed - */ - updateModifiers(modifiers: PersistentModifier[], hideHeldItems = false) { - this.removeAll(true); - - const visibleIconModifiers = modifiers.filter(m => m.isIconVisible()); - const nonPokemonSpecificModifiers = visibleIconModifiers - .filter(m => !(m as PokemonHeldItemModifier).pokemonId) - .sort(modifierSortFunc); - const pokemonSpecificModifiers = visibleIconModifiers - .filter(m => (m as PokemonHeldItemModifier).pokemonId) - .sort(modifierSortFunc); - - const sortedVisibleIconModifiers = hideHeldItems - ? nonPokemonSpecificModifiers - : nonPokemonSpecificModifiers.concat(pokemonSpecificModifiers); - - sortedVisibleIconModifiers.forEach((modifier: PersistentModifier, i: number) => { - const icon = modifier.getIcon(); - if (i >= iconOverflowIndex) { - icon.setVisible(false); - } - this.add(icon); - this.setModifierIconPosition(icon, sortedVisibleIconModifiers.length); - icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 24), Phaser.Geom.Rectangle.Contains); - icon.on("pointerover", () => { - globalScene.ui.showTooltip(modifier.type.name, modifier.type.getDescription()); - if (this.modifierCache && this.modifierCache.length > iconOverflowIndex) { - this.updateModifierOverflowVisibility(true); - } - }); - icon.on("pointerout", () => { - globalScene.ui.hideTooltip(); - if (this.modifierCache && this.modifierCache.length > iconOverflowIndex) { - this.updateModifierOverflowVisibility(false); - } - }); - }); - - for (const icon of this.getAll()) { - this.sendToBack(icon); - } - - this.modifierCache = modifiers; - } - - updateModifierOverflowVisibility(ignoreLimit: boolean) { - const modifierIcons = this.getAll().reverse(); - for (const modifier of modifierIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) { - modifier.setVisible(ignoreLimit); - } - } - - setModifierIconPosition(icon: Phaser.GameObjects.Container, modifierCount: number) { - const rowIcons: number = 12 + 6 * Math.max(Math.ceil(Math.min(modifierCount, 24) / 12) - 2, 0); - - const x = ((this.getIndex(icon) % rowIcons) * 26) / (rowIcons / 12); - const y = Math.floor(this.getIndex(icon) / rowIcons) * 20; - - icon.setPosition(this.player ? x : -x, y); - } -} - export abstract class Modifier { public type: ModifierType; @@ -661,1151 +539,6 @@ export class TerastallizeAccessModifier extends PersistentModifier { } } -export abstract class PokemonHeldItemModifier extends PersistentModifier { - /** The ID of the {@linkcode Pokemon} that this item belongs to. */ - public pokemonId: number; - /** Whether this item can be transfered to or stolen by another Pokemon. */ - public isTransferable = true; - - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, stackCount); - - this.pokemonId = pokemonId; - } - - abstract matchType(_modifier: Modifier): boolean; - - match(modifier: Modifier) { - return this.matchType(modifier) && (modifier as PokemonHeldItemModifier).pokemonId === this.pokemonId; - } - - getArgs(): any[] { - return [this.pokemonId]; - } - - /** - * Applies the {@linkcode PokemonHeldItemModifier} to the given {@linkcode Pokemon}. - * @param pokemon The {@linkcode Pokemon} that holds the held item - * @param args additional parameters - */ - abstract override apply(pokemon: Pokemon, ...args: unknown[]): boolean; - - /** - * Checks if {@linkcode PokemonHeldItemModifier} should be applied. - * @param pokemon The {@linkcode Pokemon} that holds the item - * @param _args N/A - * @returns if {@linkcode PokemonHeldItemModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, ..._args: unknown[]): boolean { - return !!pokemon && (this.pokemonId === -1 || pokemon.id === this.pokemonId); - } - - isIconVisible(): boolean { - return !!this.getPokemon()?.isOnField(); - } - - getIcon(forSummary?: boolean): Phaser.GameObjects.Container { - const container = !forSummary ? globalScene.add.container(0, 0) : super.getIcon(); - - if (!forSummary) { - const pokemon = this.getPokemon(); - if (pokemon) { - const pokemonIcon = globalScene.addPokemonIcon(pokemon, -2, 10, 0, 0.5, undefined, true); - container.add(pokemonIcon); - container.setName(pokemon.id.toString()); - } - - const item = globalScene.add.sprite(16, this.virtualStackCount ? 8 : 16, "items"); - item.setScale(0.5); - item.setOrigin(0, 0.5); - item.setTexture("items", this.type.iconImage); - container.add(item); - - const stackText = this.getIconStackText(); - if (stackText) { - container.add(stackText); - } - - const virtualStackText = this.getIconStackText(true); - if (virtualStackText) { - container.add(virtualStackText); - } - } else { - container.setScale(0.5); - } - - return container; - } - - getPokemon(): Pokemon | undefined { - return this.pokemonId ? (globalScene.getPokemonById(this.pokemonId) ?? undefined) : undefined; - } - - getScoreMultiplier(): number { - return 1; - } - - getMaxStackCount(forThreshold?: boolean): number { - const pokemon = this.getPokemon(); - if (!pokemon) { - return 0; - } - if (pokemon.isPlayer() && forThreshold) { - return globalScene - .getPlayerParty() - .map(p => this.getMaxHeldItemCount(p)) - .reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0); - } - return this.getMaxHeldItemCount(pokemon); - } - - 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: Species; - protected required: number; - public isTransferable = false; - - constructor(type: ModifierType, pokemonId: number, species: Species, 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 { - if (this.getMaxStackCount() === 1 || (virtual && !this.virtualStackCount)) { - return null; - } - - const pokemon = globalScene.getPokemonById(this.pokemonId); - - this.stackCount = pokemon - ? pokemon.evoCounter + - pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + - globalScene.findModifiers( - m => - m instanceof MoneyMultiplierModifier || - m instanceof ExtraModifierModifier || - m instanceof TempExtraModifierModifier, - ).length - : this.stackCount; - - const text = globalScene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); - text.letterSpacing = -0.5; - if (this.getStackCount() >= this.required) { - text.setTint(0xf89890); - } - text.setOrigin(0, 0); - - return text; - } - - getMaxHeldItemCount(pokemon: Pokemon): number { - this.stackCount = - pokemon.evoCounter + - pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + - globalScene.findModifiers( - m => - m instanceof MoneyMultiplierModifier || - m instanceof ExtraModifierModifier || - m instanceof TempExtraModifierModifier, - ).length; - return 999; - } -} - -/** - * Currently used by Shuckle Juice item - */ -export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { - public override type: PokemonBaseStatTotalModifierType; - public isTransferable = false; - - private statModifier: number; - - constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: number, stackCount?: number) { - super(type, pokemonId, stackCount); - this.statModifier = statModifier; - } - - override matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonBaseStatTotalModifier && this.statModifier === modifier.statModifier; - } - - override clone(): PersistentModifier { - return new PokemonBaseStatTotalModifier(this.type, this.pokemonId, this.statModifier, this.stackCount); - } - - override getArgs(): any[] { - return super.getArgs().concat(this.statModifier); - } - - /** - * Checks if {@linkcode PokemonBaseStatTotalModifier} should be applied to the specified {@linkcode Pokemon}. - * @param pokemon the {@linkcode Pokemon} to be modified - * @param baseStats the base stats of the {@linkcode Pokemon} - * @returns `true` if the {@linkcode Pokemon} should be modified - */ - override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { - return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); - } - - /** - * Applies the {@linkcode PokemonBaseStatTotalModifier} - * @param _pokemon the {@linkcode Pokemon} to be modified - * @param baseStats the base stats of the {@linkcode Pokemon} - * @returns always `true` - */ - override apply(_pokemon: Pokemon, baseStats: number[]): boolean { - // Modifies the passed in baseStats[] array - baseStats.forEach((v, i) => { - // HP is affected by half as much as other stats - const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier); - baseStats[i] = Math.min(Math.max(newVal, 1), 999999); - }); - - return true; - } - - override getScoreMultiplier(): number { - return 1.2; - } - - override getMaxHeldItemCount(_pokemon: Pokemon): number { - return 2; - } -} - -/** - * Currently used by Old Gateau item - */ -export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { - private statModifier: number; - private stats: Stat[]; - public isTransferable = false; - - constructor(type: ModifierType, pokemonId: number, statModifier: number, stats: Stat[], stackCount?: number) { - super(type, pokemonId, stackCount); - - this.statModifier = statModifier; - this.stats = stats; - } - - override matchType(modifier: Modifier): boolean { - return ( - modifier instanceof PokemonBaseStatFlatModifier && - modifier.statModifier === this.statModifier && - this.stats.every(s => modifier.stats.some(stat => s === stat)) - ); - } - - override clone(): PersistentModifier { - return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount); - } - - override getArgs(): any[] { - return [...super.getArgs(), this.statModifier, this.stats]; - } - - /** - * Checks if the {@linkcode PokemonBaseStatFlatModifier} should be applied to the {@linkcode Pokemon}. - * @param pokemon The {@linkcode Pokemon} that holds the item - * @param baseStats The base stats of the {@linkcode Pokemon} - * @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean { - return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats); - } - - /** - * Applies the {@linkcode PokemonBaseStatFlatModifier} - * @param _pokemon The {@linkcode Pokemon} that holds the item - * @param baseStats The base stats of the {@linkcode Pokemon} - * @returns always `true` - */ - override apply(_pokemon: Pokemon, baseStats: number[]): boolean { - // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats - baseStats.forEach((v, i) => { - if (this.stats.includes(i)) { - const newVal = Math.floor(v + this.statModifier); - baseStats[i] = Math.min(Math.max(newVal, 1), 999999); - } - }); - - return true; - } - - override getScoreMultiplier(): number { - return 1.1; - } - - override getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Currently used by Macho Brace item - */ -export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { - public isTransferable = false; - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonIncrementingStatModifier; - } - - clone(): PokemonIncrementingStatModifier { - return new PokemonIncrementingStatModifier(this.type, this.pokemonId, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs(); - } - - /** - * 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 { - clone() { - return super.clone() as EvolutionStatBoosterModifier; - } - - 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 Species}. - * @extends StatBoosterModifier - * @see {@linkcode apply} - */ -export class SpeciesStatBoosterModifier extends StatBoosterModifier { - /** The species that the held item's stat boost(s) apply to */ - private species: Species[]; - - constructor( - type: ModifierType, - pokemonId: number, - stats: Stat[], - multiplier: number, - species: Species[], - 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 Species} - * (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 Species} 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: Species, 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 Species}. - * @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: Species[]; - - constructor(type: ModifierType, pokemonId: number, stageIncrement: number, species: Species[], 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 Species} (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.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 instanceof PokemonHeldItemModifierType && this.type.id === "QUICK_CLAW"; - - if (isCommandFight && hasQuickClaw) { - globalScene.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 Abilities.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.unshiftPhase( - new 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.unshiftPhase( - new 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; @@ -1840,228 +573,6 @@ export class LevelIncrementBoosterModifier extends PersistentModifier { } } -export class BerryModifier extends PokemonHeldItemModifier { - public berryType: BerryType; - public consumed: boolean; - - constructor(type: ModifierType, pokemonId: number, berryType: BerryType, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.berryType = berryType; - this.consumed = false; - } - - matchType(modifier: Modifier) { - return modifier instanceof BerryModifier && (modifier as BerryModifier).berryType === this.berryType; - } - - clone() { - return new BerryModifier(this.type, this.pokemonId, this.berryType, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.berryType); - } - - /** - * Checks if {@linkcode BerryModifier} should be applied - * @param pokemon The {@linkcode Pokemon} that holds the berry - * @returns `true` if {@linkcode BerryModifier} should be applied - */ - override shouldApply(pokemon: Pokemon): boolean { - return !this.consumed && super.shouldApply(pokemon) && getBerryPredicate(this.berryType)(pokemon); - } - - /** - * Applies {@linkcode BerryModifier} - * @param pokemon The {@linkcode Pokemon} that holds the berry - * @returns always `true` - */ - override apply(pokemon: Pokemon): boolean { - const preserve = new BooleanHolder(false); - globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); - this.consumed = !preserve.value; - - // munch the berry and trigger unburden-like effects - getBerryEffectFunc(this.berryType)(pokemon); - applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); - - // Update berry eaten trackers for Belch, Harvest, Cud Chew, etc. - // Don't recover it if we proc berry pouch (no item duplication) - pokemon.recordEatenBerry(this.berryType, this.consumed); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - if ([BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(this.berryType)) { - return 2; - } - return 3; - } -} - -export class PreserveBerryModifier extends PersistentModifier { - match(modifier: Modifier) { - return modifier instanceof PreserveBerryModifier; - } - - 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.unshiftPhase( - new PokemonHealPhase( - pokemon.getBattlerIndex(), - toDmgValue(pokemon.getMaxHp() / 2), - i18next.t("modifier:pokemonInstantReviveApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - false, - false, - true, - ), - ); - - // Remove the Pokemon's FAINT status - pokemon.resetStatus(true, false, true, false); - - // Reapply Commander on the Pokemon's side of the field, if applicable - const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - for (const p of field) { - applyAbAttrs(CommanderAbAttr, p, null, false); - } - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Modifier used for held items, namely White Herb, that restore adverse stat - * stages in battle. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof ResetNegativeStatStageModifier; - } - - clone() { - return new ResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Goes through the holder's stat stages and, if any are negative, resets that - * stat stage back to 0. - * @param pokemon {@linkcode Pokemon} that holds the item - * @returns `true` if any stat stages were reset, false otherwise - */ - override apply(pokemon: Pokemon): boolean { - let statRestored = false; - - for (const s of BATTLE_STATS) { - if (pokemon.getStatStage(s) < 0) { - pokemon.setStatStage(s, 0); - statRestored = true; - } - } - - if (statRestored) { - globalScene.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; @@ -2554,59 +1065,6 @@ export class ExpBoosterModifier extends PersistentModifier { } } -export class PokemonExpBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonExpBoosterModifierType; - - private boostMultiplier: number; - - constructor(type: PokemonExpBoosterModifierType, pokemonId: number, boostPercent: number, stackCount?: number) { - super(type, pokemonId, stackCount); - this.boostMultiplier = boostPercent * 0.01; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof PokemonExpBoosterModifier) { - const pokemonExpModifier = modifier as PokemonExpBoosterModifier; - return pokemonExpModifier.boostMultiplier === this.boostMultiplier; - } - return false; - } - - clone(): PersistentModifier { - return new PokemonExpBoosterModifier(this.type, this.pokemonId, this.boostMultiplier * 100, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.boostMultiplier * 100); - } - - /** - * Checks if {@linkcode PokemonExpBoosterModifier} should be applied - * @param pokemon The {@linkcode Pokemon} to apply the exp boost to - * @param boost {@linkcode NumberHolder} holding the exp boost value - * @returns `true` if {@linkcode PokemonExpBoosterModifier} should be applied - */ - override shouldApply(pokemon: Pokemon, boost: NumberHolder): boolean { - return super.shouldApply(pokemon, boost) && !!boost; - } - - /** - * Applies {@linkcode PokemonExpBoosterModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the exp boost to - * @param boost {@linkcode NumberHolder} holding the exp boost value - * @returns always `true` - */ - override apply(_pokemon: Pokemon, boost: NumberHolder): boolean { - boost.value = Math.floor(boost.value * (1 + this.getStackCount() * this.boostMultiplier)); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 99; - } -} - export class ExpShareModifier extends PersistentModifier { match(modifier: Modifier): boolean { return modifier instanceof ExpShareModifier; @@ -2651,258 +1109,6 @@ export class ExpBalanceModifier extends PersistentModifier { } } -export class PokemonFriendshipBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonFriendshipBoosterModifierType; - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonFriendshipBoosterModifier; - } - - clone(): PersistentModifier { - return new PokemonFriendshipBoosterModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode PokemonFriendshipBoosterModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the friendship boost to - * @param friendship {@linkcode NumberHolder} holding the friendship boost value - * @returns always `true` - */ - override apply(_pokemon: Pokemon, friendship: NumberHolder): boolean { - friendship.value = Math.floor(friendship.value * (1 + 0.5 * this.getStackCount())); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -export class PokemonNatureWeightModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonNatureWeightModifier; - } - - clone(): PersistentModifier { - return new PokemonNatureWeightModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode PokemonNatureWeightModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the nature weight to - * @param multiplier {@linkcode NumberHolder} holding the nature weight - * @returns `true` if multiplier was applied - */ - override apply(_pokemon: Pokemon, multiplier: NumberHolder): boolean { - if (multiplier.value !== 1) { - multiplier.value += 0.1 * this.getStackCount() * (multiplier.value > 1 ? 1 : -1); - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 10; - } -} - -export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonMoveAccuracyBoosterModifierType; - private accuracyAmount: number; - - constructor(type: PokemonMoveAccuracyBoosterModifierType, pokemonId: number, accuracy: number, stackCount?: number) { - super(type, pokemonId, stackCount); - this.accuracyAmount = accuracy; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof PokemonMoveAccuracyBoosterModifier) { - const pokemonAccuracyBoosterModifier = modifier as PokemonMoveAccuracyBoosterModifier; - return pokemonAccuracyBoosterModifier.accuracyAmount === this.accuracyAmount; - } - return false; - } - - clone(): PersistentModifier { - return new PokemonMoveAccuracyBoosterModifier(this.type, this.pokemonId, this.accuracyAmount, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.accuracyAmount); - } - - /** - * Checks if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied - * @param pokemon The {@linkcode Pokemon} to apply the move accuracy boost to - * @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost - * @returns `true` if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, moveAccuracy?: NumberHolder): boolean { - return super.shouldApply(pokemon, moveAccuracy) && !!moveAccuracy; - } - - /** - * Applies {@linkcode PokemonMoveAccuracyBoosterModifier} - * @param _pokemon The {@linkcode Pokemon} to apply the move accuracy boost to - * @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost - * @returns always `true` - */ - override apply(_pokemon: Pokemon, moveAccuracy: NumberHolder): boolean { - moveAccuracy.value = moveAccuracy.value + this.accuracyAmount * this.getStackCount(); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -export class PokemonMultiHitModifier extends PokemonHeldItemModifier { - public override type: PokemonMultiHitModifierType; - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonMultiHitModifier; - } - - clone(): PersistentModifier { - return new PokemonMultiHitModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * For each stack, converts 25 percent of attack damage into an additional strike. - * @param pokemon The {@linkcode Pokemon} using the move - * @param moveId The {@linkcode Moves | 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: Moves, - count: NumberHolder | null = null, - damageMultiplier: NumberHolder | null = null, - ): boolean { - const move = allMoves[moveId]; - /** - * The move must meet Parental Bond's restrictions for this item - * to apply. This means - * - Only attacks are boosted - * - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted - * (though Multi-Lens can still affect moves boosted by Parental Bond) - * - Multi-target moves are not boosted *unless* they can only hit a single Pokemon - * - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted - */ - if (!move.canBeMultiStrikeEnhanced(pokemon)) { - return false; - } - - if (!isNullOrUndefined(count)) { - return this.applyHitCountBoost(count); - } - if (!isNullOrUndefined(damageMultiplier)) { - return this.applyDamageModifier(pokemon, damageMultiplier); - } - - return false; - } - - /** Adds strikes to a move equal to the number of stacked Multi-Lenses */ - private applyHitCountBoost(count: NumberHolder): boolean { - count.value += this.getStackCount(); - return true; - } - - /** - * If applied to the first hit of a move, sets the damage multiplier - * equal to (1 - the number of stacked Multi-Lenses). - * Additional strikes beyond that are given a 0.25x damage multiplier - */ - private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean { - if (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) { - // Reduce first hit by 25% for each stack count - damageMultiplier.value *= 1 - 0.25 * this.getStackCount(); - return true; - } - if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== this.getStackCount() + 1) { - // Deal 25% damage for each remaining Multi Lens hit - damageMultiplier.value *= 0.25; - return true; - } - // An extra hit not caused by Multi Lens -- assume it is Parental Bond - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 2; - } -} - -export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier { - public override type: FormChangeItemModifierType; - public formChangeItem: FormChangeItem; - public active: boolean; - public isTransferable = false; - - constructor( - type: FormChangeItemModifierType, - pokemonId: number, - formChangeItem: FormChangeItem, - active: boolean, - stackCount?: number, - ) { - super(type, pokemonId, stackCount); - this.formChangeItem = formChangeItem; - this.active = active; - } - - matchType(modifier: Modifier): boolean { - return modifier instanceof PokemonFormChangeItemModifier && modifier.formChangeItem === this.formChangeItem; - } - - clone(): PersistentModifier { - return new PokemonFormChangeItemModifier( - this.type, - this.pokemonId, - this.formChangeItem, - this.active, - this.stackCount, - ); - } - - getArgs(): any[] { - return super.getArgs().concat(this.formChangeItem, this.active); - } - - /** - * Applies {@linkcode PokemonFormChangeItemModifier} - * @param pokemon The {@linkcode Pokemon} to apply the form change item to - * @param active `true` if the form change item is active - * @returns `true` if the form change item was applied - */ - override apply(pokemon: Pokemon, active: boolean): boolean { - const switchActive = this.active && !active; - - if (switchActive) { - this.active = false; - } - - const ret = globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger); - - if (switchActive) { - this.active = true; - } - - return ret; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - export class MoneyRewardModifier extends ConsumableModifier { private moneyMultiplier: number; @@ -2964,34 +1170,6 @@ export class MoneyMultiplierModifier extends PersistentModifier { } } -export class DamageMoneyRewardModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof DamageMoneyRewardModifier; - } - - clone(): DamageMoneyRewardModifier { - return new DamageMoneyRewardModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode DamageMoneyRewardModifier} - * @param pokemon The {@linkcode Pokemon} attacking - * @param multiplier {@linkcode NumberHolder} holding the multiplier value - * @returns always `true` - */ - override apply(_pokemon: Pokemon, multiplier: NumberHolder): boolean { - const moneyAmount = new NumberHolder(Math.floor(multiplier.value * (0.5 * this.getStackCount()))); - globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); - globalScene.addMoney(moneyAmount.value); - - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 5; - } -} - export class MoneyInterestModifier extends PersistentModifier { match(modifier: Modifier): boolean { return modifier instanceof MoneyInterestModifier; @@ -3103,6 +1281,42 @@ export class CriticalCatchChanceBoosterModifier extends PersistentModifier { } } +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 LockModifierTiersModifier extends PersistentModifier { match(modifier: Modifier): boolean { return modifier instanceof LockModifierTiersModifier; @@ -3187,204 +1401,6 @@ export class BoostBugSpawnModifier extends PersistentModifier { } } -export class SwitchEffectTransferModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof SwitchEffectTransferModifier; - } - - clone(): SwitchEffectTransferModifier { - return new SwitchEffectTransferModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode SwitchEffectTransferModifier} - * @returns always `true` - */ - override apply(): boolean { - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Abstract class for held items that steal other Pokemon's items. - * @see {@linkcode TurnHeldItemTransferModifier} - * @see {@linkcode ContactHeldItemTransferChanceModifier} - */ -export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { - /** - * Determines the targets to transfer items from when this applies. - * @param pokemon the {@linkcode Pokemon} holding this item - * @param _args N/A - * @returns the opponents of the source {@linkcode Pokemon} - */ - getTargets(pokemon?: Pokemon, ..._args: unknown[]): Pokemon[] { - return pokemon instanceof Pokemon ? pokemon.getOpponents() : []; - } - - /** - * Steals an item from a set of target Pokemon. - * This prioritizes high-tier held items when selecting the item to steal. - * @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 poolType = pokemon.isPlayer() - ? ModifierPoolType.PLAYER - : pokemon.hasTrainer() - ? ModifierPoolType.TRAINER - : ModifierPoolType.WILD; - - const transferredModifierTypes: ModifierType[] = []; - const itemModifiers = globalScene.findModifiers( - m => m instanceof PokemonHeldItemModifier && m.pokemonId === targetPokemon.id && m.isTransferable, - targetPokemon.isPlayer(), - ) as PokemonHeldItemModifier[]; - let highestItemTier = itemModifiers - .map(m => m.type.getOrInferTier(poolType)) - .reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct? - let tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier); - - for (let i = 0; i < transferredItemCount; i++) { - if (!tierItemModifiers.length) { - while (highestItemTier-- && !tierItemModifiers.length) { - tierItemModifiers = itemModifiers.filter(m => m.type.tier === highestItemTier); - } - if (!tierItemModifiers.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.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 Phaser.Math.RND.realInRange(0, 1) < 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); @@ -3828,46 +1844,3 @@ export function overrideModifiers(isPlayer = true): void { } } } - -/** - * 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 instanceof 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); - } - } - } -} diff --git a/src/phases/add-enemy-buff-modifier-phase.ts b/src/phases/add-enemy-buff-modifier-phase.ts index 16ed78e6d0d..3fd44b144f0 100644 --- a/src/phases/add-enemy-buff-modifier-phase.ts +++ b/src/phases/add-enemy-buff-modifier-phase.ts @@ -1,12 +1,9 @@ import { ModifierTier } from "#app/modifier/modifier-tier"; -import { - regenerateModifierPoolThresholds, - ModifierPoolType, - getEnemyBuffModifierForWave, -} from "#app/modifier/modifier-type"; +import { regenerateModifierPoolThresholds, getEnemyBuffModifierForWave } from "#app/modifier/modifier-pool"; import { EnemyPersistentModifier } from "#app/modifier/modifier"; import { Phase } from "#app/phase"; import { globalScene } from "#app/global-scene"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; export class AddEnemyBuffModifierPhase extends Phase { start() { diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 8592cd98508..c6376c34f53 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -12,7 +12,7 @@ import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import type { EnemyPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; import { VictoryPhase } from "#app/phases/victory-phase"; import { achvs } from "#app/system/achv"; diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index b4bb28fe55e..fe00f86d75b 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -1,8 +1,9 @@ import { globalScene } from "#app/global-scene"; import { applyPostBattleAbAttrs, PostBattleAbAttr } from "#app/data/abilities/ability"; -import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; +import { LapsingPersistentModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; import { GameOverPhase } from "./game-over-phase"; +import { LapsingPokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; export class BattleEndPhase extends BattlePhase { /** If true, will increment battles won */ diff --git a/src/phases/berry-phase.ts b/src/phases/berry-phase.ts index b027469ea5e..a6c34ec338f 100644 --- a/src/phases/berry-phase.ts +++ b/src/phases/berry-phase.ts @@ -7,7 +7,7 @@ import { import { CommonAnim } from "#app/data/battle-anims"; import { BerryUsedEvent } from "#app/events/battle-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import { BerryModifier } from "#app/modifier/modifier"; +import { BerryModifier } from "#app/modifier/held-item-modifier"; import i18next from "i18next"; import { BooleanHolder } from "#app/utils/common"; import { FieldPhase } from "./field-phase"; diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 3cfd2b9a901..f8503cd5352 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -19,8 +19,8 @@ import { EncounterPhaseEvent } from "#app/events/battle-scene"; import type Pokemon from "#app/field/pokemon"; import { FieldPosition } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { BoostBugSpawnModifier, IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; -import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { BoostBugSpawnModifier, IvScannerModifier } from "#app/modifier/modifier"; +import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-pool"; import Overrides from "#app/overrides"; import { BattlePhase } from "#app/phases/battle-phase"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; @@ -41,10 +41,13 @@ import { Biome } from "#enums/biome"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; -import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; +import { overrideModifiers } from "#app/modifier/modifier"; import i18next from "i18next"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; import { getNatureName } from "#app/data/nature"; +import { TurnHeldItemTransferModifier } from "#app/modifier/held-item-modifier"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; +import { overrideHeldItems } from "#app/modifier/modifier-type"; export class EncounterPhase extends BattlePhase { private loaded: boolean; diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index bf0adf77061..da10394dc06 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -20,7 +20,7 @@ import type { EnemyPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { HitResult, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; +import { PokemonInstantReviveModifier } from "#app/modifier/held-item-modifier"; import { SwitchType } from "#enums/switch-type"; import i18next from "i18next"; import { DamageAnimPhase } from "./damage-anim-phase"; diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index e3773952214..c2c8ef6d725 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -55,12 +55,10 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { ContactHeldItemTransferChanceModifier, DamageMoneyRewardModifier, - EnemyAttackStatusEffectChanceModifier, - EnemyEndureChanceModifier, FlinchChanceModifier, HitHealModifier, PokemonMultiHitModifier, -} from "#app/modifier/modifier"; +} from "#app/modifier/held-item-modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#app/utils/common"; import type { nil } from "#app/utils/common"; @@ -78,6 +76,7 @@ import type Move from "#app/data/moves/move"; import { isFieldTargeted } from "#app/data/moves/move-utils"; import { FaintPhase } from "./faint-phase"; import { DamageAchv } from "#app/system/achv"; +import { EnemyAttackStatusEffectChanceModifier, EnemyEndureChanceModifier } from "#app/modifier/modifier"; type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 5f11441333b..aa88a8d9e4a 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -2,8 +2,6 @@ import { globalScene } from "#app/global-scene"; import type { ModifierTier } from "#app/modifier/modifier-tier"; import type { ModifierTypeOption, ModifierType } from "#app/modifier/modifier-type"; import { - regenerateModifierPoolThresholds, - getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, @@ -11,16 +9,15 @@ import { RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, - ModifierPoolType, - getPlayerModifierTypeOptions, } from "#app/modifier/modifier-type"; -import type { Modifier } from "#app/modifier/modifier"; import { - ExtraModifierModifier, - HealShopCostModifier, - PokemonHeldItemModifier, - TempExtraModifierModifier, -} from "#app/modifier/modifier"; + regenerateModifierPoolThresholds, + getPlayerShopModifierTypeOptionsForWave, + getPlayerModifierTypeOptions, + type CustomModifierSettings, +} from "#app/modifier/modifier-pool"; +import type { Modifier } from "#app/modifier/modifier"; +import { ExtraModifierModifier, HealShopCostModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; import type ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; @@ -28,8 +25,9 @@ import { UiMode } from "#enums/ui-mode"; import i18next from "i18next"; import { BattlePhase } from "./battle-phase"; import Overrides from "#app/overrides"; -import type { CustomModifierSettings } from "#app/modifier/modifier-type"; import { isNullOrUndefined, NumberHolder } from "#app/utils/common"; +import { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; export class SelectModifierPhase extends BattlePhase { private rerollCount: number; diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index 0a76df31a2c..b30e148aa1a 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -3,7 +3,7 @@ import { applyChallenges, ChallengeType } from "#app/data/challenge"; import { Gender } from "#app/data/gender"; import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms"; import { getPokemonSpecies } from "#app/data/pokemon-species"; -import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; +import { overrideModifiers } from "#app/modifier/modifier"; import Overrides from "#app/overrides"; import { Phase } from "#app/phase"; import { TitlePhase } from "#app/phases/title-phase"; @@ -13,6 +13,7 @@ import { UiMode } from "#enums/ui-mode"; import type { Species } from "#enums/species"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import { isNullOrUndefined } from "#app/utils/common"; +import { overrideHeldItems } from "#app/modifier/modifier-type"; export class SelectStarterPhase extends Phase { start() { diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 6731e45025c..e62e8845821 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -15,7 +15,7 @@ import { ArenaTagSide, MistTag } from "#app/data/arena-tag"; import type { ArenaTag } from "#app/data/arena-tag"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; +import { ResetNegativeStatStageModifier } from "#app/modifier/held-item-modifier"; import { handleTutorial, Tutorial } from "#app/tutorial"; import { NumberHolder, BooleanHolder, isNullOrUndefined } from "#app/utils/common"; import i18next from "i18next"; diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 6bdbb66be14..94a3044ce24 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -13,7 +13,7 @@ import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { TrainerSlot } from "#enums/trainer-slot"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { SwitchEffectTransferModifier } from "#app/modifier/modifier"; +import { SwitchEffectTransferModifier } from "#app/modifier/held-item-modifier"; import { Command } from "#app/ui/command-ui-handler"; import i18next from "i18next"; import { PostSummonPhase } from "./post-summon-phase"; @@ -138,7 +138,6 @@ export class SwitchSummonPhase extends SummonPhase { return; } - if (this.switchType === SwitchType.BATON_PASS) { // If switching via baton pass, update opposing tags coming from the prior pokemon (this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach((enemyPokemon: Pokemon) => diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 56057c23372..0ec2228d5ce 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -5,12 +5,8 @@ import { Gender } from "#app/data/gender"; import { getBiomeKey } from "#app/field/arena"; import { GameMode, GameModes, getGameMode } from "#app/game-mode"; import type { Modifier } from "#app/modifier/modifier"; -import { - getDailyRunStarterModifiers, - ModifierPoolType, - modifierTypes, - regenerateModifierPoolThresholds, -} from "#app/modifier/modifier-type"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#app/modifier/modifier-pool"; import { Phase } from "#app/phase"; import type { SessionSaveData } from "#app/system/game-data"; import { Unlockables } from "#app/system/unlockables"; @@ -27,6 +23,7 @@ import { SelectStarterPhase } from "./select-starter-phase"; import { SummonPhase } from "./summon-phase"; import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; +import { ModifierPoolType } from "#app/modifier/modifier-pool-type"; export class TitlePhase extends Phase { private loaded = false; diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 756c497802b..f0af24d81cf 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -5,17 +5,16 @@ import { WeatherType } from "#app/enums/weather-type"; import { TurnEndEvent } from "#app/events/battle-scene"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { - TurnHealModifier, - EnemyTurnHealModifier, - EnemyStatusEffectHealChanceModifier, - TurnStatusEffectModifier, - TurnHeldItemTransferModifier, -} from "#app/modifier/modifier"; +import { EnemyTurnHealModifier, EnemyStatusEffectHealChanceModifier } from "#app/modifier/modifier"; import i18next from "i18next"; import { FieldPhase } from "./field-phase"; import { PokemonHealPhase } from "./pokemon-heal-phase"; import { globalScene } from "#app/global-scene"; +import { + TurnHealModifier, + TurnHeldItemTransferModifier, + TurnStatusEffectModifier, +} from "#app/modifier/held-item-modifier"; export class TurnEndPhase extends FieldPhase { start() { diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index de510ef07d7..6ebb408fdef 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -5,7 +5,7 @@ import { Abilities } from "#app/enums/abilities"; import { Stat } from "#app/enums/stat"; import type Pokemon from "#app/field/pokemon"; import { PokemonMove } from "#app/field/pokemon"; -import { BypassSpeedChanceModifier } from "#app/modifier/modifier"; +import { BypassSpeedChanceModifier } from "#app/modifier/held-item-modifier"; import { Command } from "#app/ui/command-ui-handler"; import { randSeedShuffle, BooleanHolder } from "#app/utils/common"; import { AttemptCapturePhase } from "./attempt-capture-phase"; diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 1204877fec2..a65e9f0c2f2 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -1,7 +1,7 @@ import type { BattlerIndex } from "#app/battle"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { BattleType } from "#enums/battle-type"; -import type { CustomModifierSettings } from "#app/modifier/modifier-type"; +import type { CustomModifierSettings } from "#app/modifier/modifier-pool"; import { modifierTypes } from "#app/modifier/modifier-type"; import { BattleEndPhase } from "./battle-end-phase"; import { NewBattlePhase } from "./new-battle-phase"; diff --git a/src/system/achv.ts b/src/system/achv.ts index 90816ff65c3..97f41835be6 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -1,5 +1,5 @@ import type { Modifier } from "typescript"; -import { TurnHeldItemTransferModifier } from "../modifier/modifier"; +import { TurnHeldItemTransferModifier } from "../modifier/held-item-modifier"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import i18next from "i18next"; import { NumberHolder } from "#app/utils/common"; diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 7f5bf997f88..412af08f116 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -1,11 +1,11 @@ import { globalScene } from "#app/global-scene"; import type { ModifierTypeOption } from "../modifier/modifier-type"; -import { getPlayerShopModifierTypeOptionsForWave, TmModifierType } from "../modifier/modifier-type"; +import { TmModifierType } from "../modifier/modifier-type"; import { getPokeballAtlasKey } from "#app/data/pokeball"; import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; import AwaitableUiHandler from "./awaitable-ui-handler"; import { UiMode } from "#enums/ui-mode"; -import { LockModifierTiersModifier, PokemonHeldItemModifier, HealShopCostModifier } from "../modifier/modifier"; +import { LockModifierTiersModifier, HealShopCostModifier } from "../modifier/modifier"; import { handleTutorial, Tutorial } from "../tutorial"; import { Button } from "#enums/buttons"; import MoveInfoOverlay from "./move-info-overlay"; @@ -16,6 +16,8 @@ import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import Phaser from "phaser"; import type { PokeballType } from "#enums/pokeball"; +import { getPlayerShopModifierTypeOptionsForWave } from "#app/modifier/modifier-pool"; +import { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; export const SHOP_OPTIONS_ROW_LIMIT = 7; const SINGLE_SHOP_ROW_YOFFSET = 12; diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 74d4e5bcb17..6244da75297 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -10,7 +10,7 @@ import { PokemonFormChangeItemModifier, PokemonHeldItemModifier, SwitchEffectTransferModifier, -} from "#app/modifier/modifier"; +} from "#app/modifier/held-item-modifier"; import { ForceSwitchOutAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender"; @@ -1165,8 +1165,7 @@ export default class PartyUiHandler extends MessageUiHandler { this.partyUiMode !== PartyUiMode.FAINT_SWITCH && globalScene.findModifier( m => - m instanceof SwitchEffectTransferModifier && - m.pokemonId === globalScene.getPlayerField()[this.fieldIndex].id, + m instanceof SwitchEffectTransferModifier && m.pokemonId === globalScene.getPlayerField()[this.fieldIndex].id, ) ); } diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 8487533f465..c7953eefcf3 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -12,7 +12,7 @@ import { Button } from "../enums/buttons"; import { BattleType } from "#enums/battle-type"; import { TrainerVariant } from "../field/trainer"; import { Challenges } from "#enums/challenges"; -import { getLuckString, getLuckTextTint } from "../modifier/modifier-type"; +import { getLuckString, getLuckTextTint } from "../modifier/modifier-utils"; import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle"; import { getTypeRgb } from "#app/data/type"; import { PokemonType } from "#enums/pokemon-type"; @@ -27,6 +27,8 @@ import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { getBiomeName } from "#app/data/balance/biomes"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; +import { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; +import { modifierSortFunc } from "#app/modifier/modifier-bar"; /** * RunInfoUiMode indicates possible overlays of RunInfoUiHandler. @@ -650,7 +652,7 @@ export default class RunInfoUiHandler extends UiHandler { modifierIconsContainer.setScale(0.45); for (const m of this.runInfo.modifiers) { const modifier = m.toModifier(this.modifiersModule[m.className]); - if (modifier instanceof Modifier.PokemonHeldItemModifier) { + if (modifier instanceof PokemonHeldItemModifier) { continue; } const icon = modifier?.getIcon(false); @@ -881,17 +883,17 @@ export default class RunInfoUiHandler extends UiHandler { const heldItemsScale = this.runInfo.gameMode === GameModes.SPLICED_ENDLESS || this.runInfo.gameMode === GameModes.ENDLESS ? 0.25 : 0.5; const heldItemsContainer = globalScene.add.container(-82, 2); - const heldItemsList: Modifier.PokemonHeldItemModifier[] = []; + const heldItemsList: 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) { + if (modifier instanceof PokemonHeldItemModifier && modifier.pokemonId === pokemon.id) { modifier.stackCount = m["stackCount"]; heldItemsList.push(modifier); } } if (heldItemsList.length > 0) { - (heldItemsList as Modifier.PokemonHeldItemModifier[]).sort(Modifier.modifierSortFunc); + (heldItemsList as PokemonHeldItemModifier[]).sort(modifierSortFunc); let row = 0; for (const [index, item] of heldItemsList.entries()) { if (index > 36) { diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 7b4d46203c9..122b1b6c2db 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -12,6 +12,7 @@ import { TextStyle, addTextObject } from "./text"; import { UiMode } from "#enums/ui-mode"; import { addWindow } from "./ui-theme"; import { RunDisplayMode } from "#app/ui/run-info-ui-handler"; +import { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; const SESSION_SLOTS_COUNT = 5; const SLOTS_ON_SCREEN = 3; @@ -445,7 +446,7 @@ class SessionSlot extends Phaser.GameObjects.Container { let visibleModifierIndex = 0; for (const m of data.modifiers) { const modifier = m.toModifier(Modifier[m.className]); - if (modifier instanceof Modifier.PokemonHeldItemModifier) { + if (modifier instanceof PokemonHeldItemModifier) { continue; } const icon = modifier?.getIcon(false); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index a6f640b436f..581357ca662 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -24,7 +24,6 @@ import { MoveCategory } from "#enums/MoveCategory"; import { getPokeballAtlasKey } from "#app/data/pokeball"; import { getGenderColor, getGenderSymbol } from "#app/data/gender"; import { getLevelRelExp, getLevelTotalExp } from "#app/data/exp"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { StatusEffect } from "#enums/status-effect"; import { getBiomeName } from "#app/data/balance/biomes"; import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; @@ -34,11 +33,12 @@ import { getVariantTint } from "#app/sprites/variant"; import { Button } from "#enums/buttons"; import type { Ability } from "#app/data/abilities/ability-class"; import i18next from "i18next"; -import { modifierSortFunc } from "#app/modifier/modifier"; import { PlayerGender } from "#enums/player-gender"; import { Stat, PERMANENT_STATS, getStatKey } from "#enums/stat"; import { Nature } from "#enums/nature"; import { achvs } from "#app/system/achv"; +import { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; +import { modifierSortFunc } from "#app/modifier/modifier-bar"; enum Page { PROFILE, diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index 5e14e5f7771..ffc51d36c3a 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -6,7 +6,7 @@ import { getMoveTargets } from "../data/moves/move"; import { Button } from "#enums/buttons"; import type { Moves } from "#enums/moves"; import type Pokemon from "#app/field/pokemon"; -import type { ModifierBar } from "#app/modifier/modifier"; +import type { ModifierBar } from "#app/modifier/modifier-bar"; import { SubstituteTag } from "#app/data/battler-tags"; import { globalScene } from "#app/global-scene"; diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts index 36b1b879598..1e4c93a6ee1 100644 --- a/test/abilities/harvest.test.ts +++ b/test/abilities/harvest.test.ts @@ -1,7 +1,8 @@ import { BattlerIndex } from "#app/battle"; import { PostTurnRestoreBerryAbAttr } from "#app/data/abilities/ability"; import type Pokemon from "#app/field/pokemon"; -import { BerryModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { BerryModifier } from "#app/modifier/held-item-modifier"; +import { PreserveBerryModifier } from "#app/modifier/modifier"; import type { ModifierOverride } from "#app/modifier/modifier-type"; import type { BooleanHolder } from "#app/utils/common"; import { Abilities } from "#enums/abilities"; diff --git a/test/abilities/unburden.test.ts b/test/abilities/unburden.test.ts index ea4f84545aa..ff092b4aff6 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -3,7 +3,7 @@ import { PostItemLostAbAttr } from "#app/data/abilities/ability"; import { StealHeldItemChanceAttr } from "#app/data/moves/move"; import { allMoves } from "#app/data/data-lists"; import type Pokemon from "#app/field/pokemon"; -import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; +import type {} from "#app/modifier/modifier"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -13,6 +13,7 @@ import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/held-item-modifier"; describe("Abilities - Unburden", () => { let phaserGame: Phaser.Game; diff --git a/test/achievements/achievement.test.ts b/test/achievements/achievement.test.ts index 0b49c4d23ab..22c4d670e6f 100644 --- a/test/achievements/achievement.test.ts +++ b/test/achievements/achievement.test.ts @@ -1,4 +1,4 @@ -import { TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { TurnHeldItemTransferModifier } from "#app/modifier/held-item-modifier"; import { Achv, AchvTier, diff --git a/test/final_boss.test.ts b/test/final_boss.test.ts index 1b0cdce60a0..e19e85149fd 100644 --- a/test/final_boss.test.ts +++ b/test/final_boss.test.ts @@ -1,5 +1,5 @@ import { GameModes } from "#app/game-mode"; -import { TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { TurnHeldItemTransferModifier } from "#app/modifier/held-item-modifier"; import { Abilities } from "#enums/abilities"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; diff --git a/test/items/eviolite.test.ts b/test/items/eviolite.test.ts index fafc0f4a10c..e9e9574cbf3 100644 --- a/test/items/eviolite.test.ts +++ b/test/items/eviolite.test.ts @@ -1,4 +1,4 @@ -import { StatBoosterModifier } from "#app/modifier/modifier"; +import { StatBoosterModifier } from "#app/modifier/held-item-modifier"; import { NumberHolder, randItem } from "#app/utils/common"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; diff --git a/test/items/exp_booster.test.ts b/test/items/exp_booster.test.ts index 106574b6849..f4928824fa2 100644 --- a/test/items/exp_booster.test.ts +++ b/test/items/exp_booster.test.ts @@ -1,5 +1,5 @@ import { Abilities } from "#app/enums/abilities"; -import { PokemonExpBoosterModifier } from "#app/modifier/modifier"; +import { PokemonExpBoosterModifier } from "#app/modifier/held-item-modifier"; import { NumberHolder } from "#app/utils/common"; import GameManager from "#test/testUtils/gameManager"; import Phase from "phaser"; diff --git a/test/items/grip_claw.test.ts b/test/items/grip_claw.test.ts index 2396a7ca072..ca46cba6c0c 100644 --- a/test/items/grip_claw.test.ts +++ b/test/items/grip_claw.test.ts @@ -1,6 +1,7 @@ import { BattlerIndex } from "#app/battle"; import type Pokemon from "#app/field/pokemon"; -import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; +import type { ContactHeldItemTransferChanceModifier } from "#app/modifier/held-item-modifier"; +import type {} from "#app/modifier/modifier"; import { Abilities } from "#enums/abilities"; import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; diff --git a/test/items/light_ball.test.ts b/test/items/light_ball.test.ts index 214f6f624e6..89dbaeca89e 100644 --- a/test/items/light_ball.test.ts +++ b/test/items/light_ball.test.ts @@ -1,5 +1,5 @@ import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; +import { SpeciesStatBoosterModifier } from "#app/modifier/held-item-modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; import { NumberHolder } from "#app/utils/common"; diff --git a/test/items/metal_powder.test.ts b/test/items/metal_powder.test.ts index a9a81072622..8c8bc2a296c 100644 --- a/test/items/metal_powder.test.ts +++ b/test/items/metal_powder.test.ts @@ -1,5 +1,5 @@ import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; +import { SpeciesStatBoosterModifier } from "#app/modifier/held-item-modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; import { NumberHolder } from "#app/utils/common"; diff --git a/test/items/quick_powder.test.ts b/test/items/quick_powder.test.ts index fb08d6bc71e..37296563355 100644 --- a/test/items/quick_powder.test.ts +++ b/test/items/quick_powder.test.ts @@ -1,5 +1,5 @@ import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; +import { SpeciesStatBoosterModifier } from "#app/modifier/held-item-modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; import { NumberHolder } from "#app/utils/common"; diff --git a/test/items/reviver_seed.test.ts b/test/items/reviver_seed.test.ts index 13aaf98249e..031ded8a354 100644 --- a/test/items/reviver_seed.test.ts +++ b/test/items/reviver_seed.test.ts @@ -1,7 +1,7 @@ import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/data-lists"; import { BattlerTagType } from "#app/enums/battler-tag-type"; -import type { PokemonInstantReviveModifier } from "#app/modifier/modifier"; +import type { PokemonInstantReviveModifier } from "#app/modifier/held-item-modifier"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; diff --git a/test/items/thick_club.test.ts b/test/items/thick_club.test.ts index 350735a363c..1aeae5f06e5 100644 --- a/test/items/thick_club.test.ts +++ b/test/items/thick_club.test.ts @@ -1,5 +1,5 @@ import { Stat } from "#enums/stat"; -import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; +import { SpeciesStatBoosterModifier } from "#app/modifier/held-item-modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; import { NumberHolder, randInt } from "#app/utils/common"; diff --git a/test/moves/destiny_bond.test.ts b/test/moves/destiny_bond.test.ts index 16014677f39..53b2fa1974b 100644 --- a/test/moves/destiny_bond.test.ts +++ b/test/moves/destiny_bond.test.ts @@ -10,7 +10,7 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { BattlerIndex } from "#app/battle"; import { StatusEffect } from "#enums/status-effect"; -import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; +import { PokemonInstantReviveModifier } from "#app/modifier/held-item-modifier"; describe("Moves - Destiny Bond", () => { let phaserGame: Phaser.Game; diff --git a/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index 36a284880c1..164e1a100b5 100644 --- a/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -12,7 +12,7 @@ import type BattleScene from "#app/battle-scene"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; -import { BerryModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { BerryModifier, PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; import { BerryType } from "#enums/berry-type"; import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter"; import { Moves } from "#enums/moves"; diff --git a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 3f85b0b89d9..a9ced452829 100644 --- a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -11,7 +11,7 @@ import { import type BattleScene from "#app/battle-scene"; import { UiMode } from "#enums/ui-mode"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; -import { BerryModifier } from "#app/modifier/modifier"; +import { BerryModifier } from "#app/modifier/held-item-modifier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; 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 455a5d28194..93d4040e3b3 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -18,12 +18,13 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; import { TrainerType } from "#enums/trainer-type"; import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; -import { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; +import {} from "#app/modifier/modifier"; import { CommandPhase } from "#app/phases/command-phase"; import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter"; import * as encounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { ContactHeldItemTransferChanceModifier } from "#app/modifier/held-item-modifier"; const namespace = "mysteryEncounters/bugTypeSuperfan"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.WEEDLE]; @@ -116,20 +117,14 @@ const POOL_3_POKEMON: { species: Species; formIndex?: number }[] = [ const POOL_4_POKEMON = [Species.GENESECT, Species.SLITHER_WING, Species.BUZZWOLE, Species.PHEROMOSA]; -const PHYSICAL_TUTOR_MOVES = [ - Moves.MEGAHORN, - Moves.ATTACK_ORDER, - Moves.BUG_BITE, - Moves.FIRST_IMPRESSION, - Moves.LUNGE -]; +const PHYSICAL_TUTOR_MOVES = [Moves.MEGAHORN, Moves.ATTACK_ORDER, Moves.BUG_BITE, Moves.FIRST_IMPRESSION, Moves.LUNGE]; const SPECIAL_TUTOR_MOVES = [ Moves.SILVER_WIND, Moves.SIGNAL_BEAM, Moves.BUG_BUZZ, Moves.POLLEN_PUFF, - Moves.STRUGGLE_BUG + Moves.STRUGGLE_BUG, ]; const STATUS_TUTOR_MOVES = [ @@ -137,16 +132,10 @@ const STATUS_TUTOR_MOVES = [ Moves.DEFEND_ORDER, Moves.RAGE_POWDER, Moves.STICKY_WEB, - Moves.SILK_TRAP + Moves.SILK_TRAP, ]; -const MISC_TUTOR_MOVES = [ - Moves.LEECH_LIFE, - Moves.U_TURN, - Moves.HEAL_ORDER, - Moves.QUIVER_DANCE, - Moves.INFESTATION, -]; +const MISC_TUTOR_MOVES = [Moves.LEECH_LIFE, Moves.U_TURN, Moves.HEAL_ORDER, Moves.QUIVER_DANCE, Moves.INFESTATION]; describe("Bug-Type Superfan - Mystery Encounter", () => { let phaserGame: Phaser.Game; diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index afc4a83e9bf..7378173a12f 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -31,7 +31,7 @@ import type OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handle import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type"; import { BerryType } from "#enums/berry-type"; -import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import type { PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; import { PokemonType } from "#enums/pokemon-type"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; diff --git a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 94faf070a39..6c61f4b6a39 100644 --- a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -16,18 +16,20 @@ import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encount import type { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { BerryModifier, - HealingBoosterModifier, HitHealModifier, - LevelIncrementBoosterModifier, - MoneyMultiplierModifier, PokemonInstantReviveModifier, PokemonNatureWeightModifier, - PreserveBerryModifier, -} from "#app/modifier/modifier"; +} from "#app/modifier/held-item-modifier"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; import { BerryType } from "#enums/berry-type"; +import { + HealingBoosterModifier, + LevelIncrementBoosterModifier, + MoneyMultiplierModifier, + PreserveBerryModifier, +} from "#app/modifier/modifier"; const namespace = "mysteryEncounters/delibirdy"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; diff --git a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 3d311134d4e..595aea2b72e 100644 --- a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -16,7 +16,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { Moves } from "#enums/moves"; import type BattleScene from "#app/battle-scene"; -import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#app/modifier/held-item-modifier"; import { PokemonType } from "#enums/pokemon-type"; import { Status } from "#app/data/status-effect"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; 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 576e99c4e18..5d658f8ab0a 100644 --- a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -9,7 +9,6 @@ import type BattleScene from "#app/battle-scene"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; -import { PokemonNatureWeightModifier } from "#app/modifier/modifier"; import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter"; @@ -19,6 +18,7 @@ import { UiMode } from "#enums/ui-mode"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { ModifierTier } from "#app/modifier/modifier-tier"; import * as Utils from "#app/utils/common"; +import { PokemonNatureWeightModifier } from "#app/modifier/held-item-modifier"; const namespace = "mysteryEncounters/globalTradeSystem"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; 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 e3440aee9e0..e4e4c8a33b7 100644 --- a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -20,7 +20,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { PokemonMove } from "#app/field/pokemon"; import { UiMode } from "#enums/ui-mode"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; -import { BerryModifier, PokemonBaseStatTotalModifier } from "#app/modifier/modifier"; +import { BerryModifier, PokemonBaseStatTotalModifier } from "#app/modifier/held-item-modifier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; 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 2f910a9250f..a03d3d5bb9c 100644 --- a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -13,7 +13,7 @@ import { Biome } from "#app/enums/biome"; import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; import { Species } from "#app/enums/species"; import { PokemonMove } from "#app/field/pokemon"; -import { HealShopCostModifier, HitHealModifier, TurnHealModifier } from "#app/modifier/modifier"; +import { HitHealModifier, TurnHealModifier } from "#app/modifier/held-item-modifier"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { modifierTypes, type PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { CommandPhase } from "#app/phases/command-phase"; @@ -32,6 +32,7 @@ import { import GameManager from "#test/testUtils/gameManager"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { HealShopCostModifier } from "#app/modifier/modifier"; const namespace = "mysteryEncounters/trashToTreasure"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; diff --git a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index 452dfcf3784..6cce69b8919 100644 --- a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -25,7 +25,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species"; import { BerryType } from "#enums/berry-type"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; -import type { BerryModifier } from "#app/modifier/modifier"; +import type { BerryModifier } from "#app/modifier/held-item-modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import { Abilities } from "#enums/abilities"; diff --git a/test/phases/select-modifier-phase.test.ts b/test/phases/select-modifier-phase.test.ts index 85f8b472c4a..b7c9dd70818 100644 --- a/test/phases/select-modifier-phase.test.ts +++ b/test/phases/select-modifier-phase.test.ts @@ -2,7 +2,7 @@ import type BattleScene from "#app/battle-scene"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { PlayerPokemon } from "#app/field/pokemon"; import { ModifierTier } from "#app/modifier/modifier-tier"; -import type { CustomModifierSettings } from "#app/modifier/modifier-type"; +import type { CustomModifierSettings } from "#app/modifier/modifier-pool"; import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; diff --git a/test/testUtils/helpers/modifiersHelper.ts b/test/testUtils/helpers/modifiersHelper.ts index 22500c87906..d5183153a12 100644 --- a/test/testUtils/helpers/modifiersHelper.ts +++ b/test/testUtils/helpers/modifiersHelper.ts @@ -1,7 +1,7 @@ import { expect } from "vitest"; import { GameManagerHelper } from "./gameManagerHelper"; import type { ModifierTypeKeys } from "#app/modifier/modifier-type"; -import { itemPoolChecks } from "#app/modifier/modifier-type"; +import { itemPoolChecks } from "#app/modifier/modifier-pool"; export class ModifierHelper extends GameManagerHelper { /**