Held item refactor (#5656)

* Introducing held items

* Pokemon class can add held items

* Example of applyHeldItemAttrs

* Introducing a PokemonItemManager class

* Moving away from HeldItemAttr

* Attempt at incorporating the new framework in modifier-type

* Changes

* More changes

* Splitting up methods in select-modifier-phase.ts

* Newrefactors of reward-pool-manager.ts

* New refactors of select-modifier-phase.ts

* Extracted logic from modifierSelectCallback

* Moved some files around, added many held item ids

* Introduced HeldItemReward class

* Introduced AttackBoosterReward

* Introduced AttackBoosterRewardGenerator

* Removed unused file

* Select modifier phase tentatively working with new held items

* Working leftovers in new style

* Moved modifier-bar to its own file

* Held items now show up in summary

* Added some suggestions

* Implemented more suggestions

* Splitting up held-item file

* Fixing various imports

* Created items folder

* Shell bell

* Converted White Herb

* HeldItem .apply() methods do not really need the stack as input, just the pokemon

* Made heldItems public in heldItemManager

* Update modifier bar is now called in the apply of consumable held items

* Refactored parameters passed to .apply() methods; introduced generic .applyHeldItems() function; all HeldItems classes specify an ITEM_EFFECT

* Lucky egg and Golden egg

* Converted wild pokemon pool to held item rewards

* Temporary stopgap on maxUpgradeCount to avoid game crashing on modifier select ui handler

* Changed held-items.ts to held-item-id.ts and renamed id object accoridngly

* Added reviver seed

* Simplified HeldItemReward

* Added effect of reviver seed (leveraging consumable logic)

* Remove InstantReviveModifier

* Added Stat Boost items; generic name and description in HeldItem class

* Added Crit Boost held items, King's Rock, Focus Band and Quick Claw

* Added Mystical Rock

* Added Shell Bell, Soul Dew

* Added multi lens and wide lens

* Added Baton and Golden Punch

* Baton switch logic in party ui handler now using held item

* Partial implementation of item steal items

* Using held items in some places

* Using phaseManager

* Tracking forms in held item manager

* Shuckle Juice and Old Gateau

* Using phaseManager

* Removed a bunch of modifiers

* Fixed shell bell in ability.ts (why is it here?)

* Changed BattleScene.removeModifier and pokemon.loseHeldItem

* Making some held items unstealable and unsuppressable

* Refactored most of battle-scene.ts with held items

* Added soundName to HeldItem (possibly useless)

* Reworked various effects that steal items

* Refactored Baton logic

* Reworked most entries in Modifier Types

* pokemon.getHeldItems now uses heldItemManager

* Added Evolution Tracker as held item

* MBE is always untransferable

* Improved item transfer

* Fixed types in held item manager

* Various fixes

* Fixed types in shuckle juice and old gateau

* MBE achievement now tracks held items

* Removed AttackTypeBoosterModifierRequirement for MEs

* Fixed Pickup

* Fixing (most) berry usage

* Using Berry held items in move.ts

* Split up vitamins from the rest of stat boosting items

* Fixed form change trigger after merge conflicts

* Added some utility functions to check if an item fits a list of items/categories, or to filter out which held items fit

* Fixed delibirdy encounter

* Various fixes

* Reworked EnemyPokemonConfig to include a HeldItemProperty object (to feed to the heldItemManager). Updated Dark Deal ME

* More various fixes; introduced isMaxStack(item) method to heldItemManager

* Updated modifier-bar.ts

* Converted Berries Abound encounter

* Converted The Strong Stuff encounter

* Fixed Slumbering Snorlax encounter, overrideItems of heldItemConfiguration can deal with items with 0 stack

* Preliminary changes to some MEs

* Changes to the summary ui handler

* Converting to held item pools

* Fixed evo tracker

* Moved many data types for held items to a dedicated file; introduced held item pools for item generation

* Fixed some MEs

* Added function to assign items from a given configuration

* Fixed held item evolution condition

* Fixed some ui handlers

* Fixed select-modifier-phase

* Some changes to modifier files

* Daily run items are generated within the new system

* Held item generation for enemies follows the new scheme

* Fixed init-modifier-pools.ts

* Held item overrides now use the new system

* Removed unused getOrInferTier function (was only used by thief and covet)

* Fixed shady vitamin ME and some HeldItem files

* Updated various MEs. HeldItemManager can now generate a configuration from its items.

* Converted most MEs, introduced some more utility functions and methods

* Replaced ModifierTier with RewardTier

* Fixed some rogue inputs

* Held item pools take a single pokemon as input, set weights to 0 for max stack items

* Restored various changes to held item pool generation; distinguishing target of the generation from the party

* Pools can now take a HaldItemConfigurationId without weights; set up table of item tiers; used custom pools in Clowning Around ME

* Fixing some phases

* Fixed several bugs related to accessing and visualizing held items

* Fixed position of enemy modifier bar

* Fixed berries, removed some debug messages

* Vitamins max stack temporarily set to 30

* Luck upgrades work correctly again (maybe)

* First steps to port tests to the new system

* Form change items work correctly when selected in battle

* Introducing helper functions for a modifier migrator; PokemonItemMap now uses pokemonId

* Renamed file with item migrator functions

* Restored missing strings

* Shuckle juic and Old gateau are not instance dependent

* Simplified HeldItemData

* Migrator utils include shuckle juice

* Introducing trainer items

* Enemy tokens are now also trainer items

* Converted most of modifierTypes

* Fixed various MEs; removed findModifier from battle-scene

* Removed most uses of globalScene.applyModifiers

* Removed more functions from globalScene; changed lure weight function

* Updated a variety of files

* Split out X_accuracy, fixed trainer-item-manager

* Fixed expert breeder ME

* Replaced updateModifiers with updateItems

* Removed modifiers from saveData

* Trainer item rewards are generating during runs, added properly to the scene

* Items (including held items) are saved and loaded correctly.

* Fixed several trainer item names and descriptions, plus various issues with item generation

* Restored override file

* Added icons to tokens

* Fixed Dire Hit starting stack

* Using Pokemon.getMoveType() in generation of attack type boosters

* Test for dire hit; ensuring that lapsing trainer items are added from config at full stack

* Eviolite does not apply to G-Max forms

* Changes to various item tests

* Some still broken tests

* More changes to tests but still failing

* Fixed evo tracker item

* Moved allHeldItems and allTrainerItems definitions to data-lists

* Renamed modifier-bar and moved to ui files

* Fixed held item when pokemon not passed

* Remove `package-lock.json` added by merge issue

* Add review comments as `TODO`s for now

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Wlowscha 2025-07-10 09:54:59 +02:00 committed by NightKev
parent 8c05693b83
commit 7e2418b957
168 changed files with 6866 additions and 7908 deletions

View File

@ -1,5 +1,5 @@
import type { EnemyPokemon } from "#app/field/pokemon"; import type { EnemyPokemon } from "#app/field/pokemon";
import type { PersistentModifier } from "#app/modifier/modifier"; import type { TrainerItemConfiguration } from "#app/items/trainer-item-data-types";
import type { PartyMemberStrength } from "#enums/party-member-strength"; import type { PartyMemberStrength } from "#enums/party-member-strength";
import type { SpeciesId } from "#enums/species-id"; import type { SpeciesId } from "#enums/species-id";
import type { TrainerConfig } from "../data/trainers/trainer-config"; import type { TrainerConfig } from "../data/trainers/trainer-config";
@ -7,7 +7,7 @@ import type { TrainerPartyTemplate } from "../data/trainers/TrainerPartyTemplate
export type PartyTemplateFunc = () => TrainerPartyTemplate; export type PartyTemplateFunc = () => TrainerPartyTemplate;
export type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon; export type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon;
export type GenModifiersFunc = (party: EnemyPokemon[]) => PersistentModifier[]; export type GenTrainerItemsFunc = (party: EnemyPokemon[]) => TrainerItemConfiguration;
export type GenAIFunc = (party: EnemyPokemon[]) => void; export type GenAIFunc = (party: EnemyPokemon[]) => void;
export interface TrainerTierPools { export interface TrainerTierPools {

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,6 @@ import {
import Trainer from "./field/trainer"; import Trainer from "./field/trainer";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import type { GameMode } from "./game-mode"; import type { GameMode } from "./game-mode";
import { MoneyMultiplierModifier, type PokemonHeldItemModifier } from "./modifier/modifier";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { trainerConfigs } from "#app/data/trainers/trainer-config";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
@ -31,11 +30,13 @@ import i18next from "#app/plugins/i18n";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import type { CustomModifierSettings } from "#app/modifier/modifier-type"; import type { CustomModifierSettings } from "#app/modifier/modifier-type";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import type { HeldItemId } from "#enums/held-item-id";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { TRAINER_ITEM_EFFECT } from "./items/trainer-item";
export interface TurnCommand { export interface TurnCommand {
command: Command; command: Command;
@ -72,7 +73,7 @@ export default class Battle {
public turnCommands: TurnCommands; public turnCommands: TurnCommands;
public playerParticipantIds: Set<number> = new Set<number>(); public playerParticipantIds: Set<number> = new Set<number>();
public battleScore = 0; public battleScore = 0;
public postBattleLoot: PokemonHeldItemModifier[] = []; public postBattleLoot: HeldItemId[] = [];
public escapeAttempts = 0; public escapeAttempts = 0;
public lastMove: MoveId; public lastMove: MoveId;
public battleSeed: string = randomString(16, true); public battleSeed: string = randomString(16, true);
@ -177,24 +178,12 @@ export default class Battle {
} }
addPostBattleLoot(enemyPokemon: EnemyPokemon): void { addPostBattleLoot(enemyPokemon: EnemyPokemon): void {
this.postBattleLoot.push( this.postBattleLoot.push(...enemyPokemon.getHeldItems());
...globalScene
.findModifiers(
m => m.is("PokemonHeldItemModifier") && m.pokemonId === enemyPokemon.id && m.isTransferable,
false,
)
.map(i => {
const ret = i as PokemonHeldItemModifier;
//@ts-expect-error - this is awful to fix/change
ret.pokemonId = null;
return ret;
}),
);
} }
pickUpScatteredMoney(): void { pickUpScatteredMoney(): void {
const moneyAmount = new NumberHolder(globalScene.currentBattle.moneyScattered); const moneyAmount = new NumberHolder(globalScene.currentBattle.moneyScattered);
globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER, { numberHolder: moneyAmount });
if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) { if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) {
moneyAmount.value *= 2; moneyAmount.value *= 2;
@ -616,7 +605,7 @@ export const classicFixedBattles: FixedBattleConfigs = {
), ),
) )
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.GREAT, RewardTier.GREAT],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
[ClassicFixedBossWaves.EVIL_GRUNT_1]: new FixedBattleConfig() [ClassicFixedBossWaves.EVIL_GRUNT_1]: new FixedBattleConfig()
@ -648,7 +637,7 @@ export const classicFixedBattles: FixedBattleConfigs = {
), ),
) )
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.GREAT, RewardTier.GREAT],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
[ClassicFixedBossWaves.EVIL_GRUNT_2]: new FixedBattleConfig() [ClassicFixedBossWaves.EVIL_GRUNT_2]: new FixedBattleConfig()
@ -721,7 +710,7 @@ export const classicFixedBattles: FixedBattleConfigs = {
), ),
) )
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.ULTRA],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
[ClassicFixedBossWaves.EVIL_GRUNT_4]: new FixedBattleConfig() [ClassicFixedBossWaves.EVIL_GRUNT_4]: new FixedBattleConfig()
@ -784,11 +773,11 @@ export const classicFixedBattles: FixedBattleConfigs = {
) )
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
@ -803,11 +792,11 @@ export const classicFixedBattles: FixedBattleConfigs = {
) )
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
@ -830,12 +819,12 @@ export const classicFixedBattles: FixedBattleConfigs = {
) )
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
@ -934,12 +923,12 @@ export const classicFixedBattles: FixedBattleConfigs = {
) )
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.GREAT, RewardTier.GREAT,
ModifierTier.GREAT, RewardTier.GREAT,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),

View File

@ -22,7 +22,6 @@ import { Gender } from "#app/data/gender";
import { applyMoveAttrs } from "../moves/apply-attrs"; import { applyMoveAttrs } from "../moves/apply-attrs";
import { allMoves } from "../data-lists"; import { allMoves } from "../data-lists";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { TerrainType } from "#app/data/terrain"; import { TerrainType } from "#app/data/terrain";
import { pokemonFormChanges } from "../pokemon-forms"; import { pokemonFormChanges } from "../pokemon-forms";
import { import {
@ -31,7 +30,6 @@ import {
} from "../pokemon-forms/form-change-triggers"; } from "../pokemon-forms/form-change-triggers";
import i18next from "i18next"; import i18next from "i18next";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { BerryModifierType } from "#app/modifier/modifier-type";
import { getPokeballName } from "#app/data/pokeball"; import { getPokeballName } from "#app/data/pokeball";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
@ -79,6 +77,9 @@ import type { BattlerIndex } from "#enums/battler-index";
import type Move from "#app/data/moves/move"; import type Move from "#app/data/moves/move";
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag"; import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
import type { Constructor } from "#app/utils/common"; import type { Constructor } from "#app/utils/common";
import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { allHeldItems } from "#app/data/data-lists";
import { type BerryHeldItem, berryTypeToHeldItem } from "#app/items/held-items/berry";
import type { Localizable } from "#app/@types/locales"; import type { Localizable } from "#app/@types/locales";
import { applyAbAttrs } from "./apply-ab-attrs"; import { applyAbAttrs } from "./apply-ab-attrs";
import type { Closed, Exact } from "#app/@types/type-helpers"; import type { Closed, Exact } from "#app/@types/type-helpers";
@ -2045,7 +2046,7 @@ export abstract class PostAttackAbAttr extends AbAttr {
export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
private stealCondition: PokemonAttackCondition | null; private stealCondition: PokemonAttackCondition | null;
private stolenItem?: PokemonHeldItemModifier; private stolenItem?: HeldItemId;
constructor(stealCondition?: PokemonAttackCondition) { constructor(stealCondition?: PokemonAttackCondition) {
super(); super();
@ -2064,11 +2065,11 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
hitResult < HitResult.NO_EFFECT && hitResult < HitResult.NO_EFFECT &&
(!this.stealCondition || this.stealCondition(pokemon, opponent, move)) (!this.stealCondition || this.stealCondition(pokemon, opponent, move))
) { ) {
const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); const heldItems = opponent.heldItemManager.getTransferableHeldItems();
if (heldItems.length) { if (heldItems.length) {
// Ensure that the stolen item in testing is the same as when the effect is applied // Ensure that the stolen item in testing is the same as when the effect is applied
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { if (globalScene.canTransferHeldItem(this.stolenItem, opponent, pokemon)) {
return true; return true;
} }
} }
@ -2078,28 +2079,21 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
} }
override apply({ opponent, pokemon }: PostMoveInteractionAbAttrParams): void { override apply({ opponent, pokemon }: PostMoveInteractionAbAttrParams): void {
const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); const heldItems = opponent.heldItemManager.getTransferableHeldItems();
if (!this.stolenItem) { if (!this.stolenItem) {
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
} }
if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { if (globalScene.tryTransferHeldItem(this.stolenItem, opponent, pokemon, false)) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:postAttackStealHeldItem", { i18next.t("abilityTriggers:postAttackStealHeldItem", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
defenderName: opponent.name, defenderName: opponent.name,
stolenItemType: this.stolenItem.type.name, stolenItemType: allHeldItems[this.stolenItem].name,
}), }),
); );
} }
this.stolenItem = undefined; this.stolenItem = undefined;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === target.id,
target.isPlayer(),
) as PokemonHeldItemModifier[];
}
} }
export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
@ -2190,7 +2184,7 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr {
export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
private condition?: PokemonDefendCondition; private condition?: PokemonDefendCondition;
private stolenItem?: PokemonHeldItemModifier; private stolenItem?: HeldItemId;
constructor(condition?: PokemonDefendCondition) { constructor(condition?: PokemonDefendCondition) {
super(); super();
@ -2200,10 +2194,10 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
override canApply({ simulated, pokemon, opponent, move, hitResult }: PostMoveInteractionAbAttrParams): boolean { override canApply({ simulated, pokemon, opponent, move, hitResult }: PostMoveInteractionAbAttrParams): boolean {
if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, opponent, move))) { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, opponent, move))) {
const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); const heldItems = opponent.heldItemManager.getTransferableHeldItems();
if (heldItems.length) { if (heldItems.length) {
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { if (globalScene.canTransferHeldItem(this.stolenItem, opponent, pokemon)) {
return true; return true;
} }
} }
@ -2212,28 +2206,21 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
} }
override apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void { override apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); const heldItems = opponent.heldItemManager.getTransferableHeldItems();
if (!this.stolenItem) { if (!this.stolenItem) {
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
} }
if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { if (globalScene.tryTransferHeldItem(this.stolenItem, opponent, pokemon, false)) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:postDefendStealHeldItem", { i18next.t("abilityTriggers:postDefendStealHeldItem", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
attackerName: opponent.name, attackerName: opponent.name,
stolenItemType: this.stolenItem.type.name, stolenItemType: allHeldItems[this.stolenItem].name,
}), }),
); );
} }
this.stolenItem = undefined; this.stolenItem = undefined;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === target.id,
target.isPlayer(),
) as PokemonHeldItemModifier[];
}
} }
/** /**
@ -4590,10 +4577,14 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
override canApply({ pokemon }: AbAttrBaseParams): boolean { override canApply({ pokemon }: AbAttrBaseParams): boolean {
// Ensure we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped) // Ensure we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped)
const cappedBerries = new Set( const cappedBerries = new Set(
globalScene pokemon
.getModifiers(BerryModifier, pokemon.isPlayer()) .getHeldItems()
.filter(bm => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1) .filter(
.map(bm => bm.berryType), bm =>
isItemInCategory(bm, HeldItemCategoryId.BERRY) &&
pokemon.heldItemManager.getStack(bm) < allHeldItems[bm].maxStackCount,
)
.map(bm => (allHeldItems[bm] as BerryHeldItem).berryType),
); );
this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(bt => !cappedBerries.has(bt)); this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(bt => !cappedBerries.has(bt));
@ -4623,30 +4614,15 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
const randomIdx = randSeedInt(this.berriesUnderCap.length); const randomIdx = randSeedInt(this.berriesUnderCap.length);
const chosenBerryType = this.berriesUnderCap[randomIdx]; const chosenBerryType = this.berriesUnderCap[randomIdx];
pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory
const chosenBerry = new BerryModifierType(chosenBerryType); const chosenBerry = berryTypeToHeldItem[chosenBerryType];
// Add the randomly chosen berry or update the existing one pokemon.heldItemManager.add(chosenBerry);
const berryModifier = globalScene.findModifier(
m => m instanceof BerryModifier && m.berryType === chosenBerryType && m.pokemonId === pokemon.id,
pokemon.isPlayer(),
) as BerryModifier | undefined;
if (berryModifier) { globalScene.updateItems(pokemon.isPlayer());
berryModifier.stackCount++;
} else {
const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1);
if (pokemon.isPlayer()) {
globalScene.addModifier(newBerry);
} else {
globalScene.addEnemyModifier(newBerry);
}
}
globalScene.updateModifiers(pokemon.isPlayer());
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
berryName: chosenBerry.name, berryName: allHeldItems[chosenBerry].name,
}), }),
); );
return true; return true;
@ -4680,8 +4656,7 @@ export class CudChewConsumeBerryAbAttr extends AbAttr {
// This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed. // This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed.
for (const berryType of pokemon.summonData.berriesEatenLast) { for (const berryType of pokemon.summonData.berriesEatenLast) {
getBerryEffectFunc(berryType)(pokemon); getBerryEffectFunc(berryType)(pokemon);
const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1); globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(pokemon, berryType)); // trigger message
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message
} }
// uncomment to make cheek pouch work with cud chew // uncomment to make cheek pouch work with cud chew
@ -5266,13 +5241,13 @@ export abstract class PostBattleAbAttr extends AbAttr {
} }
export class PostBattleLootAbAttr extends PostBattleAbAttr { export class PostBattleLootAbAttr extends PostBattleAbAttr {
private randItem?: PokemonHeldItemModifier; private randItem?: HeldItemId;
override canApply({ simulated, victory, pokemon }: PostBattleAbAttrParams): boolean { override canApply({ simulated, victory, pokemon }: PostBattleAbAttrParams): boolean {
const postBattleLoot = globalScene.currentBattle.postBattleLoot; const postBattleLoot = globalScene.currentBattle.postBattleLoot;
if (!simulated && postBattleLoot.length && victory) { if (!simulated && postBattleLoot.length && victory) {
this.randItem = randSeedItem(postBattleLoot); this.randItem = randSeedItem(postBattleLoot);
return globalScene.canTransferHeldItemModifier(this.randItem, pokemon, 1); return pokemon.heldItemManager.getStack(this.randItem) < allHeldItems[this.randItem].maxStackCount;
} }
return false; return false;
} }
@ -5283,12 +5258,12 @@ export class PostBattleLootAbAttr extends PostBattleAbAttr {
this.randItem = randSeedItem(postBattleLoot); this.randItem = randSeedItem(postBattleLoot);
} }
if (globalScene.tryTransferHeldItemModifier(this.randItem, pokemon, true, 1, true, undefined, false)) { if (pokemon.heldItemManager.add(this.randItem)) {
postBattleLoot.splice(postBattleLoot.indexOf(this.randItem), 1); postBattleLoot.splice(postBattleLoot.indexOf(this.randItem), 1);
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:postBattleLoot", { i18next.t("abilityTriggers:postBattleLoot", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
itemName: this.randItem.type.name, itemName: allHeldItems[this.randItem].name,
}), }),
); );
} }
@ -6187,8 +6162,6 @@ class ForceSwitchOutHelper {
} }
if (!allyPokemon?.isActive(true)) { if (!allyPokemon?.isActive(true)) {
globalScene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) { if (switchOutTarget.hp) {
globalScene.phaseManager.pushNew("BattleEndPhase", false); globalScene.phaseManager.pushNew("BattleEndPhase", false);
@ -6272,11 +6245,9 @@ class ForceSwitchOutHelper {
* @returns The amount of health recovered by Shell Bell. * @returns The amount of health recovered by Shell Bell.
*/ */
function calculateShellBellRecovery(pokemon: Pokemon): number { function calculateShellBellRecovery(pokemon: Pokemon): number {
const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); // Returns 0 if no Shell Bell is present
if (shellBellModifier) { const shellBellStack = pokemon.heldItemManager.getStack(HeldItemId.SHELL_BELL);
return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount; return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellStack;
}
return 0;
} }
export interface PostDamageAbAttrParams extends AbAttrBaseParams { export interface PostDamageAbAttrParams extends AbAttrBaseParams {

View File

@ -11,11 +11,11 @@ import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#app/modifier/modifier-type";
import { speciesStarterCosts } from "./starters"; import { speciesStarterCosts } from "./starters";
import i18next from "i18next"; import i18next from "i18next";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { HeldItemId } from "#enums/held-item-id";
export enum SpeciesWildEvolutionDelay { export enum SpeciesWildEvolutionDelay {
NONE, NONE,
@ -99,7 +99,7 @@ const EvoCondKey = {
SPECIES_CAUGHT: 12, SPECIES_CAUGHT: 12,
GENDER: 13, GENDER: 13,
NATURE: 14, NATURE: 14,
HELD_ITEM: 15, // Currently checks only for species stat booster items HELD_ITEM: 15,
} as const; } as const;
type EvolutionConditionData = type EvolutionConditionData =
@ -110,7 +110,7 @@ type EvolutionConditionData =
{key: typeof EvoCondKey.GENDER, gender: Gender} | {key: typeof EvoCondKey.GENDER, gender: Gender} |
{key: typeof EvoCondKey.MOVE_TYPE | typeof EvoCondKey.PARTY_TYPE, pkmnType: PokemonType} | {key: typeof EvoCondKey.MOVE_TYPE | typeof EvoCondKey.PARTY_TYPE, pkmnType: PokemonType} |
{key: typeof EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId} | {key: typeof EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId} |
{key: typeof EvoCondKey.HELD_ITEM, itemKey: SpeciesStatBoosterItem} | {key: typeof EvoCondKey.HELD_ITEM, itemKey: HeldItemId} |
{key: typeof EvoCondKey.NATURE, nature: Nature[]} | {key: typeof EvoCondKey.NATURE, nature: Nature[]} |
{key: typeof EvoCondKey.WEATHER, weather: WeatherType[]} | {key: typeof EvoCondKey.WEATHER, weather: WeatherType[]} |
{key: typeof EvoCondKey.TYROGUE, move: TyrogueMove} | {key: typeof EvoCondKey.TYROGUE, move: TyrogueMove} |
@ -201,7 +201,7 @@ export class SpeciesEvolutionCondition {
case EvoCondKey.SPECIES_CAUGHT: case EvoCondKey.SPECIES_CAUGHT:
return !!globalScene.gameData.dexData[cond.speciesCaught].caughtAttr; return !!globalScene.gameData.dexData[cond.speciesCaught].caughtAttr;
case EvoCondKey.HELD_ITEM: case EvoCondKey.HELD_ITEM:
return pokemon.getHeldItems().some(m => m.is("SpeciesStatBoosterModifier") && (m.type as SpeciesStatBoosterModifierType).key === cond.itemKey) return pokemon.heldItemManager.hasItem(cond.itemKey);
} }
}); });
} }
@ -1765,8 +1765,8 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.DUSKNOIR, 1, EvolutionItem.REAPER_CLOTH, null, SpeciesWildEvolutionDelay.VERY_LONG) new SpeciesEvolution(SpeciesId.DUSKNOIR, 1, EvolutionItem.REAPER_CLOTH, null, SpeciesWildEvolutionDelay.VERY_LONG)
], ],
[SpeciesId.CLAMPERL]: [ [SpeciesId.CLAMPERL]: [
new SpeciesEvolution(SpeciesId.HUNTAIL, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: "DEEP_SEA_TOOTH"}, SpeciesWildEvolutionDelay.VERY_LONG), new SpeciesEvolution(SpeciesId.HUNTAIL, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: HeldItemId.DEEP_SEA_TOOTH}, SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesEvolution(SpeciesId.GOREBYSS, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: "DEEP_SEA_SCALE"}, SpeciesWildEvolutionDelay.VERY_LONG) new SpeciesEvolution(SpeciesId.GOREBYSS, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: HeldItemId.DEEP_SEA_SCALE}, SpeciesWildEvolutionDelay.VERY_LONG)
], ],
[SpeciesId.BOLDORE]: [ [SpeciesId.BOLDORE]: [
new SpeciesEvolution(SpeciesId.GIGALITH, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) new SpeciesEvolution(SpeciesId.GIGALITH, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG)

View File

@ -1,4 +1,4 @@
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
@ -68591,324 +68591,324 @@ function transposeTmSpecies(): SpeciesTmMoves {
export const speciesTmMoves: SpeciesTmMoves = transposeTmSpecies(); export const speciesTmMoves: SpeciesTmMoves = transposeTmSpecies();
interface TmPoolTiers { interface TmPoolTiers {
[key: number]: ModifierTier [key: number]: RewardTier
} }
export const tmPoolTiers: TmPoolTiers = { export const tmPoolTiers: TmPoolTiers = {
[MoveId.MEGA_PUNCH]: ModifierTier.GREAT, [MoveId.MEGA_PUNCH]: RewardTier.GREAT,
[MoveId.PAY_DAY]: ModifierTier.ULTRA, [MoveId.PAY_DAY]: RewardTier.ULTRA,
[MoveId.FIRE_PUNCH]: ModifierTier.GREAT, [MoveId.FIRE_PUNCH]: RewardTier.GREAT,
[MoveId.ICE_PUNCH]: ModifierTier.GREAT, [MoveId.ICE_PUNCH]: RewardTier.GREAT,
[MoveId.THUNDER_PUNCH]: ModifierTier.GREAT, [MoveId.THUNDER_PUNCH]: RewardTier.GREAT,
[MoveId.SWORDS_DANCE]: ModifierTier.COMMON, [MoveId.SWORDS_DANCE]: RewardTier.COMMON,
[MoveId.CUT]: ModifierTier.COMMON, [MoveId.CUT]: RewardTier.COMMON,
[MoveId.FLY]: ModifierTier.COMMON, [MoveId.FLY]: RewardTier.COMMON,
[MoveId.MEGA_KICK]: ModifierTier.GREAT, [MoveId.MEGA_KICK]: RewardTier.GREAT,
[MoveId.BODY_SLAM]: ModifierTier.GREAT, [MoveId.BODY_SLAM]: RewardTier.GREAT,
[MoveId.TAKE_DOWN]: ModifierTier.GREAT, [MoveId.TAKE_DOWN]: RewardTier.GREAT,
[MoveId.DOUBLE_EDGE]: ModifierTier.ULTRA, [MoveId.DOUBLE_EDGE]: RewardTier.ULTRA,
[MoveId.PIN_MISSILE]: ModifierTier.COMMON, [MoveId.PIN_MISSILE]: RewardTier.COMMON,
[MoveId.ROAR]: ModifierTier.COMMON, [MoveId.ROAR]: RewardTier.COMMON,
[MoveId.FLAMETHROWER]: ModifierTier.ULTRA, [MoveId.FLAMETHROWER]: RewardTier.ULTRA,
[MoveId.HYDRO_PUMP]: ModifierTier.ULTRA, [MoveId.HYDRO_PUMP]: RewardTier.ULTRA,
[MoveId.SURF]: ModifierTier.ULTRA, [MoveId.SURF]: RewardTier.ULTRA,
[MoveId.ICE_BEAM]: ModifierTier.ULTRA, [MoveId.ICE_BEAM]: RewardTier.ULTRA,
[MoveId.BLIZZARD]: ModifierTier.ULTRA, [MoveId.BLIZZARD]: RewardTier.ULTRA,
[MoveId.PSYBEAM]: ModifierTier.GREAT, [MoveId.PSYBEAM]: RewardTier.GREAT,
[MoveId.HYPER_BEAM]: ModifierTier.ULTRA, [MoveId.HYPER_BEAM]: RewardTier.ULTRA,
[MoveId.LOW_KICK]: ModifierTier.COMMON, [MoveId.LOW_KICK]: RewardTier.COMMON,
[MoveId.COUNTER]: ModifierTier.COMMON, [MoveId.COUNTER]: RewardTier.COMMON,
[MoveId.STRENGTH]: ModifierTier.GREAT, [MoveId.STRENGTH]: RewardTier.GREAT,
[MoveId.SOLAR_BEAM]: ModifierTier.ULTRA, [MoveId.SOLAR_BEAM]: RewardTier.ULTRA,
[MoveId.FIRE_SPIN]: ModifierTier.COMMON, [MoveId.FIRE_SPIN]: RewardTier.COMMON,
[MoveId.THUNDERBOLT]: ModifierTier.ULTRA, [MoveId.THUNDERBOLT]: RewardTier.ULTRA,
[MoveId.THUNDER_WAVE]: ModifierTier.COMMON, [MoveId.THUNDER_WAVE]: RewardTier.COMMON,
[MoveId.THUNDER]: ModifierTier.ULTRA, [MoveId.THUNDER]: RewardTier.ULTRA,
[MoveId.EARTHQUAKE]: ModifierTier.ULTRA, [MoveId.EARTHQUAKE]: RewardTier.ULTRA,
[MoveId.DIG]: ModifierTier.GREAT, [MoveId.DIG]: RewardTier.GREAT,
[MoveId.TOXIC]: ModifierTier.GREAT, [MoveId.TOXIC]: RewardTier.GREAT,
[MoveId.PSYCHIC]: ModifierTier.ULTRA, [MoveId.PSYCHIC]: RewardTier.ULTRA,
[MoveId.AGILITY]: ModifierTier.COMMON, [MoveId.AGILITY]: RewardTier.COMMON,
[MoveId.NIGHT_SHADE]: ModifierTier.COMMON, [MoveId.NIGHT_SHADE]: RewardTier.COMMON,
[MoveId.SCREECH]: ModifierTier.COMMON, [MoveId.SCREECH]: RewardTier.COMMON,
[MoveId.DOUBLE_TEAM]: ModifierTier.COMMON, [MoveId.DOUBLE_TEAM]: RewardTier.COMMON,
[MoveId.CONFUSE_RAY]: ModifierTier.COMMON, [MoveId.CONFUSE_RAY]: RewardTier.COMMON,
[MoveId.LIGHT_SCREEN]: ModifierTier.COMMON, [MoveId.LIGHT_SCREEN]: RewardTier.COMMON,
[MoveId.HAZE]: ModifierTier.COMMON, [MoveId.HAZE]: RewardTier.COMMON,
[MoveId.REFLECT]: ModifierTier.COMMON, [MoveId.REFLECT]: RewardTier.COMMON,
[MoveId.FOCUS_ENERGY]: ModifierTier.COMMON, [MoveId.FOCUS_ENERGY]: RewardTier.COMMON,
[MoveId.METRONOME]: ModifierTier.COMMON, [MoveId.METRONOME]: RewardTier.COMMON,
[MoveId.SELF_DESTRUCT]: ModifierTier.GREAT, [MoveId.SELF_DESTRUCT]: RewardTier.GREAT,
[MoveId.FIRE_BLAST]: ModifierTier.ULTRA, [MoveId.FIRE_BLAST]: RewardTier.ULTRA,
[MoveId.WATERFALL]: ModifierTier.GREAT, [MoveId.WATERFALL]: RewardTier.GREAT,
[MoveId.SWIFT]: ModifierTier.COMMON, [MoveId.SWIFT]: RewardTier.COMMON,
[MoveId.AMNESIA]: ModifierTier.COMMON, [MoveId.AMNESIA]: RewardTier.COMMON,
[MoveId.DREAM_EATER]: ModifierTier.GREAT, [MoveId.DREAM_EATER]: RewardTier.GREAT,
[MoveId.LEECH_LIFE]: ModifierTier.ULTRA, [MoveId.LEECH_LIFE]: RewardTier.ULTRA,
[MoveId.FLASH]: ModifierTier.COMMON, [MoveId.FLASH]: RewardTier.COMMON,
[MoveId.EXPLOSION]: ModifierTier.GREAT, [MoveId.EXPLOSION]: RewardTier.GREAT,
[MoveId.REST]: ModifierTier.COMMON, [MoveId.REST]: RewardTier.COMMON,
[MoveId.ROCK_SLIDE]: ModifierTier.GREAT, [MoveId.ROCK_SLIDE]: RewardTier.GREAT,
[MoveId.TRI_ATTACK]: ModifierTier.ULTRA, [MoveId.TRI_ATTACK]: RewardTier.ULTRA,
[MoveId.SUPER_FANG]: ModifierTier.COMMON, [MoveId.SUPER_FANG]: RewardTier.COMMON,
[MoveId.SUBSTITUTE]: ModifierTier.COMMON, [MoveId.SUBSTITUTE]: RewardTier.COMMON,
[MoveId.THIEF]: ModifierTier.GREAT, [MoveId.THIEF]: RewardTier.GREAT,
[MoveId.SNORE]: ModifierTier.COMMON, [MoveId.SNORE]: RewardTier.COMMON,
[MoveId.CURSE]: ModifierTier.COMMON, [MoveId.CURSE]: RewardTier.COMMON,
[MoveId.REVERSAL]: ModifierTier.COMMON, [MoveId.REVERSAL]: RewardTier.COMMON,
[MoveId.SPITE]: ModifierTier.COMMON, [MoveId.SPITE]: RewardTier.COMMON,
[MoveId.PROTECT]: ModifierTier.COMMON, [MoveId.PROTECT]: RewardTier.COMMON,
[MoveId.SCARY_FACE]: ModifierTier.COMMON, [MoveId.SCARY_FACE]: RewardTier.COMMON,
[MoveId.SLUDGE_BOMB]: ModifierTier.GREAT, [MoveId.SLUDGE_BOMB]: RewardTier.GREAT,
[MoveId.MUD_SLAP]: ModifierTier.COMMON, [MoveId.MUD_SLAP]: RewardTier.COMMON,
[MoveId.SPIKES]: ModifierTier.COMMON, [MoveId.SPIKES]: RewardTier.COMMON,
[MoveId.ICY_WIND]: ModifierTier.GREAT, [MoveId.ICY_WIND]: RewardTier.GREAT,
[MoveId.OUTRAGE]: ModifierTier.ULTRA, [MoveId.OUTRAGE]: RewardTier.ULTRA,
[MoveId.SANDSTORM]: ModifierTier.COMMON, [MoveId.SANDSTORM]: RewardTier.COMMON,
[MoveId.GIGA_DRAIN]: ModifierTier.ULTRA, [MoveId.GIGA_DRAIN]: RewardTier.ULTRA,
[MoveId.ENDURE]: ModifierTier.COMMON, [MoveId.ENDURE]: RewardTier.COMMON,
[MoveId.CHARM]: ModifierTier.COMMON, [MoveId.CHARM]: RewardTier.COMMON,
[MoveId.FALSE_SWIPE]: ModifierTier.COMMON, [MoveId.FALSE_SWIPE]: RewardTier.COMMON,
[MoveId.SWAGGER]: ModifierTier.COMMON, [MoveId.SWAGGER]: RewardTier.COMMON,
[MoveId.STEEL_WING]: ModifierTier.GREAT, [MoveId.STEEL_WING]: RewardTier.GREAT,
[MoveId.ATTRACT]: ModifierTier.COMMON, [MoveId.ATTRACT]: RewardTier.COMMON,
[MoveId.SLEEP_TALK]: ModifierTier.COMMON, [MoveId.SLEEP_TALK]: RewardTier.COMMON,
[MoveId.HEAL_BELL]: ModifierTier.COMMON, [MoveId.HEAL_BELL]: RewardTier.COMMON,
[MoveId.RETURN]: ModifierTier.ULTRA, [MoveId.RETURN]: RewardTier.ULTRA,
[MoveId.FRUSTRATION]: ModifierTier.COMMON, [MoveId.FRUSTRATION]: RewardTier.COMMON,
[MoveId.SAFEGUARD]: ModifierTier.COMMON, [MoveId.SAFEGUARD]: RewardTier.COMMON,
[MoveId.PAIN_SPLIT]: ModifierTier.COMMON, [MoveId.PAIN_SPLIT]: RewardTier.COMMON,
[MoveId.MEGAHORN]: ModifierTier.ULTRA, [MoveId.MEGAHORN]: RewardTier.ULTRA,
[MoveId.BATON_PASS]: ModifierTier.COMMON, [MoveId.BATON_PASS]: RewardTier.COMMON,
[MoveId.ENCORE]: ModifierTier.COMMON, [MoveId.ENCORE]: RewardTier.COMMON,
[MoveId.IRON_TAIL]: ModifierTier.GREAT, [MoveId.IRON_TAIL]: RewardTier.GREAT,
[MoveId.METAL_CLAW]: ModifierTier.COMMON, [MoveId.METAL_CLAW]: RewardTier.COMMON,
[MoveId.SYNTHESIS]: ModifierTier.GREAT, [MoveId.SYNTHESIS]: RewardTier.GREAT,
[MoveId.HIDDEN_POWER]: ModifierTier.GREAT, [MoveId.HIDDEN_POWER]: RewardTier.GREAT,
[MoveId.RAIN_DANCE]: ModifierTier.COMMON, [MoveId.RAIN_DANCE]: RewardTier.COMMON,
[MoveId.SUNNY_DAY]: ModifierTier.COMMON, [MoveId.SUNNY_DAY]: RewardTier.COMMON,
[MoveId.CRUNCH]: ModifierTier.GREAT, [MoveId.CRUNCH]: RewardTier.GREAT,
[MoveId.PSYCH_UP]: ModifierTier.COMMON, [MoveId.PSYCH_UP]: RewardTier.COMMON,
[MoveId.SHADOW_BALL]: ModifierTier.ULTRA, [MoveId.SHADOW_BALL]: RewardTier.ULTRA,
[MoveId.FUTURE_SIGHT]: ModifierTier.GREAT, [MoveId.FUTURE_SIGHT]: RewardTier.GREAT,
[MoveId.ROCK_SMASH]: ModifierTier.COMMON, [MoveId.ROCK_SMASH]: RewardTier.COMMON,
[MoveId.WHIRLPOOL]: ModifierTier.COMMON, [MoveId.WHIRLPOOL]: RewardTier.COMMON,
[MoveId.BEAT_UP]: ModifierTier.COMMON, [MoveId.BEAT_UP]: RewardTier.COMMON,
[MoveId.UPROAR]: ModifierTier.GREAT, [MoveId.UPROAR]: RewardTier.GREAT,
[MoveId.HEAT_WAVE]: ModifierTier.ULTRA, [MoveId.HEAT_WAVE]: RewardTier.ULTRA,
[MoveId.HAIL]: ModifierTier.COMMON, [MoveId.HAIL]: RewardTier.COMMON,
[MoveId.TORMENT]: ModifierTier.COMMON, [MoveId.TORMENT]: RewardTier.COMMON,
[MoveId.WILL_O_WISP]: ModifierTier.COMMON, [MoveId.WILL_O_WISP]: RewardTier.COMMON,
[MoveId.FACADE]: ModifierTier.GREAT, [MoveId.FACADE]: RewardTier.GREAT,
[MoveId.FOCUS_PUNCH]: ModifierTier.COMMON, [MoveId.FOCUS_PUNCH]: RewardTier.COMMON,
[MoveId.NATURE_POWER]: ModifierTier.COMMON, [MoveId.NATURE_POWER]: RewardTier.COMMON,
[MoveId.CHARGE]: ModifierTier.COMMON, [MoveId.CHARGE]: RewardTier.COMMON,
[MoveId.TAUNT]: ModifierTier.COMMON, [MoveId.TAUNT]: RewardTier.COMMON,
[MoveId.HELPING_HAND]: ModifierTier.COMMON, [MoveId.HELPING_HAND]: RewardTier.COMMON,
[MoveId.TRICK]: ModifierTier.COMMON, [MoveId.TRICK]: RewardTier.COMMON,
[MoveId.SUPERPOWER]: ModifierTier.ULTRA, [MoveId.SUPERPOWER]: RewardTier.ULTRA,
[MoveId.RECYCLE]: ModifierTier.COMMON, [MoveId.RECYCLE]: RewardTier.COMMON,
[MoveId.REVENGE]: ModifierTier.GREAT, [MoveId.REVENGE]: RewardTier.GREAT,
[MoveId.BRICK_BREAK]: ModifierTier.GREAT, [MoveId.BRICK_BREAK]: RewardTier.GREAT,
[MoveId.KNOCK_OFF]: ModifierTier.GREAT, [MoveId.KNOCK_OFF]: RewardTier.GREAT,
[MoveId.ENDEAVOR]: ModifierTier.COMMON, [MoveId.ENDEAVOR]: RewardTier.COMMON,
[MoveId.SKILL_SWAP]: ModifierTier.COMMON, [MoveId.SKILL_SWAP]: RewardTier.COMMON,
[MoveId.IMPRISON]: ModifierTier.COMMON, [MoveId.IMPRISON]: RewardTier.COMMON,
[MoveId.SECRET_POWER]: ModifierTier.COMMON, [MoveId.SECRET_POWER]: RewardTier.COMMON,
[MoveId.DIVE]: ModifierTier.GREAT, [MoveId.DIVE]: RewardTier.GREAT,
[MoveId.FEATHER_DANCE]: ModifierTier.COMMON, [MoveId.FEATHER_DANCE]: RewardTier.COMMON,
[MoveId.BLAZE_KICK]: ModifierTier.GREAT, [MoveId.BLAZE_KICK]: RewardTier.GREAT,
[MoveId.HYPER_VOICE]: ModifierTier.ULTRA, [MoveId.HYPER_VOICE]: RewardTier.ULTRA,
[MoveId.BLAST_BURN]: ModifierTier.ULTRA, [MoveId.BLAST_BURN]: RewardTier.ULTRA,
[MoveId.HYDRO_CANNON]: ModifierTier.ULTRA, [MoveId.HYDRO_CANNON]: RewardTier.ULTRA,
[MoveId.WEATHER_BALL]: ModifierTier.COMMON, [MoveId.WEATHER_BALL]: RewardTier.COMMON,
[MoveId.FAKE_TEARS]: ModifierTier.COMMON, [MoveId.FAKE_TEARS]: RewardTier.COMMON,
[MoveId.AIR_CUTTER]: ModifierTier.GREAT, [MoveId.AIR_CUTTER]: RewardTier.GREAT,
[MoveId.OVERHEAT]: ModifierTier.ULTRA, [MoveId.OVERHEAT]: RewardTier.ULTRA,
[MoveId.ROCK_TOMB]: ModifierTier.GREAT, [MoveId.ROCK_TOMB]: RewardTier.GREAT,
[MoveId.METAL_SOUND]: ModifierTier.COMMON, [MoveId.METAL_SOUND]: RewardTier.COMMON,
[MoveId.COSMIC_POWER]: ModifierTier.COMMON, [MoveId.COSMIC_POWER]: RewardTier.COMMON,
[MoveId.SIGNAL_BEAM]: ModifierTier.GREAT, [MoveId.SIGNAL_BEAM]: RewardTier.GREAT,
[MoveId.SAND_TOMB]: ModifierTier.COMMON, [MoveId.SAND_TOMB]: RewardTier.COMMON,
[MoveId.MUDDY_WATER]: ModifierTier.GREAT, [MoveId.MUDDY_WATER]: RewardTier.GREAT,
[MoveId.BULLET_SEED]: ModifierTier.GREAT, [MoveId.BULLET_SEED]: RewardTier.GREAT,
[MoveId.AERIAL_ACE]: ModifierTier.GREAT, [MoveId.AERIAL_ACE]: RewardTier.GREAT,
[MoveId.ICICLE_SPEAR]: ModifierTier.GREAT, [MoveId.ICICLE_SPEAR]: RewardTier.GREAT,
[MoveId.IRON_DEFENSE]: ModifierTier.GREAT, [MoveId.IRON_DEFENSE]: RewardTier.GREAT,
[MoveId.DRAGON_CLAW]: ModifierTier.ULTRA, [MoveId.DRAGON_CLAW]: RewardTier.ULTRA,
[MoveId.FRENZY_PLANT]: ModifierTier.ULTRA, [MoveId.FRENZY_PLANT]: RewardTier.ULTRA,
[MoveId.BULK_UP]: ModifierTier.COMMON, [MoveId.BULK_UP]: RewardTier.COMMON,
[MoveId.BOUNCE]: ModifierTier.GREAT, [MoveId.BOUNCE]: RewardTier.GREAT,
[MoveId.MUD_SHOT]: ModifierTier.GREAT, [MoveId.MUD_SHOT]: RewardTier.GREAT,
[MoveId.POISON_TAIL]: ModifierTier.GREAT, [MoveId.POISON_TAIL]: RewardTier.GREAT,
[MoveId.COVET]: ModifierTier.GREAT, [MoveId.COVET]: RewardTier.GREAT,
[MoveId.MAGICAL_LEAF]: ModifierTier.GREAT, [MoveId.MAGICAL_LEAF]: RewardTier.GREAT,
[MoveId.CALM_MIND]: ModifierTier.GREAT, [MoveId.CALM_MIND]: RewardTier.GREAT,
[MoveId.LEAF_BLADE]: ModifierTier.ULTRA, [MoveId.LEAF_BLADE]: RewardTier.ULTRA,
[MoveId.DRAGON_DANCE]: ModifierTier.GREAT, [MoveId.DRAGON_DANCE]: RewardTier.GREAT,
[MoveId.ROCK_BLAST]: ModifierTier.GREAT, [MoveId.ROCK_BLAST]: RewardTier.GREAT,
[MoveId.WATER_PULSE]: ModifierTier.GREAT, [MoveId.WATER_PULSE]: RewardTier.GREAT,
[MoveId.ROOST]: ModifierTier.GREAT, [MoveId.ROOST]: RewardTier.GREAT,
[MoveId.GRAVITY]: ModifierTier.COMMON, [MoveId.GRAVITY]: RewardTier.COMMON,
[MoveId.GYRO_BALL]: ModifierTier.COMMON, [MoveId.GYRO_BALL]: RewardTier.COMMON,
[MoveId.BRINE]: ModifierTier.GREAT, [MoveId.BRINE]: RewardTier.GREAT,
[MoveId.PLUCK]: ModifierTier.GREAT, [MoveId.PLUCK]: RewardTier.GREAT,
[MoveId.TAILWIND]: ModifierTier.GREAT, [MoveId.TAILWIND]: RewardTier.GREAT,
[MoveId.U_TURN]: ModifierTier.GREAT, [MoveId.U_TURN]: RewardTier.GREAT,
[MoveId.CLOSE_COMBAT]: ModifierTier.ULTRA, [MoveId.CLOSE_COMBAT]: RewardTier.ULTRA,
[MoveId.PAYBACK]: ModifierTier.COMMON, [MoveId.PAYBACK]: RewardTier.COMMON,
[MoveId.ASSURANCE]: ModifierTier.COMMON, [MoveId.ASSURANCE]: RewardTier.COMMON,
[MoveId.EMBARGO]: ModifierTier.COMMON, [MoveId.EMBARGO]: RewardTier.COMMON,
[MoveId.FLING]: ModifierTier.COMMON, [MoveId.FLING]: RewardTier.COMMON,
[MoveId.GASTRO_ACID]: ModifierTier.GREAT, [MoveId.GASTRO_ACID]: RewardTier.GREAT,
[MoveId.POWER_SWAP]: ModifierTier.COMMON, [MoveId.POWER_SWAP]: RewardTier.COMMON,
[MoveId.GUARD_SWAP]: ModifierTier.COMMON, [MoveId.GUARD_SWAP]: RewardTier.COMMON,
[MoveId.WORRY_SEED]: ModifierTier.GREAT, [MoveId.WORRY_SEED]: RewardTier.GREAT,
[MoveId.TOXIC_SPIKES]: ModifierTier.GREAT, [MoveId.TOXIC_SPIKES]: RewardTier.GREAT,
[MoveId.FLARE_BLITZ]: ModifierTier.ULTRA, [MoveId.FLARE_BLITZ]: RewardTier.ULTRA,
[MoveId.AURA_SPHERE]: ModifierTier.GREAT, [MoveId.AURA_SPHERE]: RewardTier.GREAT,
[MoveId.ROCK_POLISH]: ModifierTier.COMMON, [MoveId.ROCK_POLISH]: RewardTier.COMMON,
[MoveId.POISON_JAB]: ModifierTier.GREAT, [MoveId.POISON_JAB]: RewardTier.GREAT,
[MoveId.DARK_PULSE]: ModifierTier.GREAT, [MoveId.DARK_PULSE]: RewardTier.GREAT,
[MoveId.AQUA_TAIL]: ModifierTier.GREAT, [MoveId.AQUA_TAIL]: RewardTier.GREAT,
[MoveId.SEED_BOMB]: ModifierTier.GREAT, [MoveId.SEED_BOMB]: RewardTier.GREAT,
[MoveId.AIR_SLASH]: ModifierTier.GREAT, [MoveId.AIR_SLASH]: RewardTier.GREAT,
[MoveId.X_SCISSOR]: ModifierTier.GREAT, [MoveId.X_SCISSOR]: RewardTier.GREAT,
[MoveId.BUG_BUZZ]: ModifierTier.GREAT, [MoveId.BUG_BUZZ]: RewardTier.GREAT,
[MoveId.DRAGON_PULSE]: ModifierTier.GREAT, [MoveId.DRAGON_PULSE]: RewardTier.GREAT,
[MoveId.POWER_GEM]: ModifierTier.GREAT, [MoveId.POWER_GEM]: RewardTier.GREAT,
[MoveId.DRAIN_PUNCH]: ModifierTier.GREAT, [MoveId.DRAIN_PUNCH]: RewardTier.GREAT,
[MoveId.VACUUM_WAVE]: ModifierTier.COMMON, [MoveId.VACUUM_WAVE]: RewardTier.COMMON,
[MoveId.FOCUS_BLAST]: ModifierTier.GREAT, [MoveId.FOCUS_BLAST]: RewardTier.GREAT,
[MoveId.ENERGY_BALL]: ModifierTier.GREAT, [MoveId.ENERGY_BALL]: RewardTier.GREAT,
[MoveId.BRAVE_BIRD]: ModifierTier.ULTRA, [MoveId.BRAVE_BIRD]: RewardTier.ULTRA,
[MoveId.EARTH_POWER]: ModifierTier.ULTRA, [MoveId.EARTH_POWER]: RewardTier.ULTRA,
[MoveId.GIGA_IMPACT]: ModifierTier.GREAT, [MoveId.GIGA_IMPACT]: RewardTier.GREAT,
[MoveId.NASTY_PLOT]: ModifierTier.COMMON, [MoveId.NASTY_PLOT]: RewardTier.COMMON,
[MoveId.AVALANCHE]: ModifierTier.GREAT, [MoveId.AVALANCHE]: RewardTier.GREAT,
[MoveId.SHADOW_CLAW]: ModifierTier.GREAT, [MoveId.SHADOW_CLAW]: RewardTier.GREAT,
[MoveId.THUNDER_FANG]: ModifierTier.GREAT, [MoveId.THUNDER_FANG]: RewardTier.GREAT,
[MoveId.ICE_FANG]: ModifierTier.GREAT, [MoveId.ICE_FANG]: RewardTier.GREAT,
[MoveId.FIRE_FANG]: ModifierTier.GREAT, [MoveId.FIRE_FANG]: RewardTier.GREAT,
[MoveId.PSYCHO_CUT]: ModifierTier.GREAT, [MoveId.PSYCHO_CUT]: RewardTier.GREAT,
[MoveId.ZEN_HEADBUTT]: ModifierTier.GREAT, [MoveId.ZEN_HEADBUTT]: RewardTier.GREAT,
[MoveId.FLASH_CANNON]: ModifierTier.GREAT, [MoveId.FLASH_CANNON]: RewardTier.GREAT,
[MoveId.ROCK_CLIMB]: ModifierTier.GREAT, [MoveId.ROCK_CLIMB]: RewardTier.GREAT,
[MoveId.DEFOG]: ModifierTier.COMMON, [MoveId.DEFOG]: RewardTier.COMMON,
[MoveId.TRICK_ROOM]: ModifierTier.COMMON, [MoveId.TRICK_ROOM]: RewardTier.COMMON,
[MoveId.DRACO_METEOR]: ModifierTier.ULTRA, [MoveId.DRACO_METEOR]: RewardTier.ULTRA,
[MoveId.LEAF_STORM]: ModifierTier.ULTRA, [MoveId.LEAF_STORM]: RewardTier.ULTRA,
[MoveId.POWER_WHIP]: ModifierTier.ULTRA, [MoveId.POWER_WHIP]: RewardTier.ULTRA,
[MoveId.CROSS_POISON]: ModifierTier.GREAT, [MoveId.CROSS_POISON]: RewardTier.GREAT,
[MoveId.GUNK_SHOT]: ModifierTier.ULTRA, [MoveId.GUNK_SHOT]: RewardTier.ULTRA,
[MoveId.IRON_HEAD]: ModifierTier.GREAT, [MoveId.IRON_HEAD]: RewardTier.GREAT,
[MoveId.STONE_EDGE]: ModifierTier.ULTRA, [MoveId.STONE_EDGE]: RewardTier.ULTRA,
[MoveId.STEALTH_ROCK]: ModifierTier.COMMON, [MoveId.STEALTH_ROCK]: RewardTier.COMMON,
[MoveId.GRASS_KNOT]: ModifierTier.ULTRA, [MoveId.GRASS_KNOT]: RewardTier.ULTRA,
[MoveId.BUG_BITE]: ModifierTier.GREAT, [MoveId.BUG_BITE]: RewardTier.GREAT,
[MoveId.CHARGE_BEAM]: ModifierTier.GREAT, [MoveId.CHARGE_BEAM]: RewardTier.GREAT,
[MoveId.HONE_CLAWS]: ModifierTier.COMMON, [MoveId.HONE_CLAWS]: RewardTier.COMMON,
[MoveId.WONDER_ROOM]: ModifierTier.COMMON, [MoveId.WONDER_ROOM]: RewardTier.COMMON,
[MoveId.PSYSHOCK]: ModifierTier.GREAT, [MoveId.PSYSHOCK]: RewardTier.GREAT,
[MoveId.VENOSHOCK]: ModifierTier.GREAT, [MoveId.VENOSHOCK]: RewardTier.GREAT,
[MoveId.MAGIC_ROOM]: ModifierTier.COMMON, [MoveId.MAGIC_ROOM]: RewardTier.COMMON,
[MoveId.SMACK_DOWN]: ModifierTier.COMMON, [MoveId.SMACK_DOWN]: RewardTier.COMMON,
[MoveId.SLUDGE_WAVE]: ModifierTier.GREAT, [MoveId.SLUDGE_WAVE]: RewardTier.GREAT,
[MoveId.HEAVY_SLAM]: ModifierTier.GREAT, [MoveId.HEAVY_SLAM]: RewardTier.GREAT,
[MoveId.ELECTRO_BALL]: ModifierTier.GREAT, [MoveId.ELECTRO_BALL]: RewardTier.GREAT,
[MoveId.FLAME_CHARGE]: ModifierTier.GREAT, [MoveId.FLAME_CHARGE]: RewardTier.GREAT,
[MoveId.LOW_SWEEP]: ModifierTier.GREAT, [MoveId.LOW_SWEEP]: RewardTier.GREAT,
[MoveId.ACID_SPRAY]: ModifierTier.COMMON, [MoveId.ACID_SPRAY]: RewardTier.COMMON,
[MoveId.FOUL_PLAY]: ModifierTier.ULTRA, [MoveId.FOUL_PLAY]: RewardTier.ULTRA,
[MoveId.ROUND]: ModifierTier.COMMON, [MoveId.ROUND]: RewardTier.COMMON,
[MoveId.ECHOED_VOICE]: ModifierTier.COMMON, [MoveId.ECHOED_VOICE]: RewardTier.COMMON,
[MoveId.STORED_POWER]: ModifierTier.COMMON, [MoveId.STORED_POWER]: RewardTier.COMMON,
[MoveId.ALLY_SWITCH]: ModifierTier.COMMON, [MoveId.ALLY_SWITCH]: RewardTier.COMMON,
[MoveId.SCALD]: ModifierTier.GREAT, [MoveId.SCALD]: RewardTier.GREAT,
[MoveId.HEX]: ModifierTier.GREAT, [MoveId.HEX]: RewardTier.GREAT,
[MoveId.SKY_DROP]: ModifierTier.GREAT, [MoveId.SKY_DROP]: RewardTier.GREAT,
[MoveId.INCINERATE]: ModifierTier.GREAT, [MoveId.INCINERATE]: RewardTier.GREAT,
[MoveId.QUASH]: ModifierTier.COMMON, [MoveId.QUASH]: RewardTier.COMMON,
[MoveId.ACROBATICS]: ModifierTier.GREAT, [MoveId.ACROBATICS]: RewardTier.GREAT,
[MoveId.RETALIATE]: ModifierTier.GREAT, [MoveId.RETALIATE]: RewardTier.GREAT,
[MoveId.WATER_PLEDGE]: ModifierTier.GREAT, [MoveId.WATER_PLEDGE]: RewardTier.GREAT,
[MoveId.FIRE_PLEDGE]: ModifierTier.GREAT, [MoveId.FIRE_PLEDGE]: RewardTier.GREAT,
[MoveId.GRASS_PLEDGE]: ModifierTier.GREAT, [MoveId.GRASS_PLEDGE]: RewardTier.GREAT,
[MoveId.VOLT_SWITCH]: ModifierTier.GREAT, [MoveId.VOLT_SWITCH]: RewardTier.GREAT,
[MoveId.STRUGGLE_BUG]: ModifierTier.COMMON, [MoveId.STRUGGLE_BUG]: RewardTier.COMMON,
[MoveId.BULLDOZE]: ModifierTier.GREAT, [MoveId.BULLDOZE]: RewardTier.GREAT,
[MoveId.FROST_BREATH]: ModifierTier.GREAT, [MoveId.FROST_BREATH]: RewardTier.GREAT,
[MoveId.DRAGON_TAIL]: ModifierTier.GREAT, [MoveId.DRAGON_TAIL]: RewardTier.GREAT,
[MoveId.WORK_UP]: ModifierTier.COMMON, [MoveId.WORK_UP]: RewardTier.COMMON,
[MoveId.ELECTROWEB]: ModifierTier.GREAT, [MoveId.ELECTROWEB]: RewardTier.GREAT,
[MoveId.WILD_CHARGE]: ModifierTier.GREAT, [MoveId.WILD_CHARGE]: RewardTier.GREAT,
[MoveId.DRILL_RUN]: ModifierTier.GREAT, [MoveId.DRILL_RUN]: RewardTier.GREAT,
[MoveId.RAZOR_SHELL]: ModifierTier.GREAT, [MoveId.RAZOR_SHELL]: RewardTier.GREAT,
[MoveId.HEAT_CRASH]: ModifierTier.GREAT, [MoveId.HEAT_CRASH]: RewardTier.GREAT,
[MoveId.TAIL_SLAP]: ModifierTier.GREAT, [MoveId.TAIL_SLAP]: RewardTier.GREAT,
[MoveId.HURRICANE]: ModifierTier.ULTRA, [MoveId.HURRICANE]: RewardTier.ULTRA,
[MoveId.SNARL]: ModifierTier.COMMON, [MoveId.SNARL]: RewardTier.COMMON,
[MoveId.PHANTOM_FORCE]: ModifierTier.ULTRA, [MoveId.PHANTOM_FORCE]: RewardTier.ULTRA,
[MoveId.PETAL_BLIZZARD]: ModifierTier.GREAT, [MoveId.PETAL_BLIZZARD]: RewardTier.GREAT,
[MoveId.DISARMING_VOICE]: ModifierTier.GREAT, [MoveId.DISARMING_VOICE]: RewardTier.GREAT,
[MoveId.DRAINING_KISS]: ModifierTier.GREAT, [MoveId.DRAINING_KISS]: RewardTier.GREAT,
[MoveId.GRASSY_TERRAIN]: ModifierTier.COMMON, [MoveId.GRASSY_TERRAIN]: RewardTier.COMMON,
[MoveId.MISTY_TERRAIN]: ModifierTier.COMMON, [MoveId.MISTY_TERRAIN]: RewardTier.COMMON,
[MoveId.PLAY_ROUGH]: ModifierTier.GREAT, [MoveId.PLAY_ROUGH]: RewardTier.GREAT,
[MoveId.CONFIDE]: ModifierTier.COMMON, [MoveId.CONFIDE]: RewardTier.COMMON,
[MoveId.MYSTICAL_FIRE]: ModifierTier.GREAT, [MoveId.MYSTICAL_FIRE]: RewardTier.GREAT,
[MoveId.EERIE_IMPULSE]: ModifierTier.COMMON, [MoveId.EERIE_IMPULSE]: RewardTier.COMMON,
[MoveId.VENOM_DRENCH]: ModifierTier.COMMON, [MoveId.VENOM_DRENCH]: RewardTier.COMMON,
[MoveId.ELECTRIC_TERRAIN]: ModifierTier.COMMON, [MoveId.ELECTRIC_TERRAIN]: RewardTier.COMMON,
[MoveId.DAZZLING_GLEAM]: ModifierTier.ULTRA, [MoveId.DAZZLING_GLEAM]: RewardTier.ULTRA,
[MoveId.INFESTATION]: ModifierTier.COMMON, [MoveId.INFESTATION]: RewardTier.COMMON,
[MoveId.POWER_UP_PUNCH]: ModifierTier.GREAT, [MoveId.POWER_UP_PUNCH]: RewardTier.GREAT,
[MoveId.DARKEST_LARIAT]: ModifierTier.GREAT, [MoveId.DARKEST_LARIAT]: RewardTier.GREAT,
[MoveId.HIGH_HORSEPOWER]: ModifierTier.ULTRA, [MoveId.HIGH_HORSEPOWER]: RewardTier.ULTRA,
[MoveId.SOLAR_BLADE]: ModifierTier.GREAT, [MoveId.SOLAR_BLADE]: RewardTier.GREAT,
[MoveId.THROAT_CHOP]: ModifierTier.GREAT, [MoveId.THROAT_CHOP]: RewardTier.GREAT,
[MoveId.POLLEN_PUFF]: ModifierTier.GREAT, [MoveId.POLLEN_PUFF]: RewardTier.GREAT,
[MoveId.PSYCHIC_TERRAIN]: ModifierTier.COMMON, [MoveId.PSYCHIC_TERRAIN]: RewardTier.COMMON,
[MoveId.LUNGE]: ModifierTier.GREAT, [MoveId.LUNGE]: RewardTier.GREAT,
[MoveId.SPEED_SWAP]: ModifierTier.COMMON, [MoveId.SPEED_SWAP]: RewardTier.COMMON,
[MoveId.SMART_STRIKE]: ModifierTier.GREAT, [MoveId.SMART_STRIKE]: RewardTier.GREAT,
[MoveId.BRUTAL_SWING]: ModifierTier.GREAT, [MoveId.BRUTAL_SWING]: RewardTier.GREAT,
[MoveId.AURORA_VEIL]: ModifierTier.COMMON, [MoveId.AURORA_VEIL]: RewardTier.COMMON,
[MoveId.PSYCHIC_FANGS]: ModifierTier.GREAT, [MoveId.PSYCHIC_FANGS]: RewardTier.GREAT,
[MoveId.STOMPING_TANTRUM]: ModifierTier.GREAT, [MoveId.STOMPING_TANTRUM]: RewardTier.GREAT,
[MoveId.LIQUIDATION]: ModifierTier.ULTRA, [MoveId.LIQUIDATION]: RewardTier.ULTRA,
[MoveId.BODY_PRESS]: ModifierTier.ULTRA, [MoveId.BODY_PRESS]: RewardTier.ULTRA,
[MoveId.BREAKING_SWIPE]: ModifierTier.GREAT, [MoveId.BREAKING_SWIPE]: RewardTier.GREAT,
[MoveId.STEEL_BEAM]: ModifierTier.ULTRA, [MoveId.STEEL_BEAM]: RewardTier.ULTRA,
[MoveId.EXPANDING_FORCE]: ModifierTier.GREAT, [MoveId.EXPANDING_FORCE]: RewardTier.GREAT,
[MoveId.STEEL_ROLLER]: ModifierTier.COMMON, [MoveId.STEEL_ROLLER]: RewardTier.COMMON,
[MoveId.SCALE_SHOT]: ModifierTier.ULTRA, [MoveId.SCALE_SHOT]: RewardTier.ULTRA,
[MoveId.METEOR_BEAM]: ModifierTier.GREAT, [MoveId.METEOR_BEAM]: RewardTier.GREAT,
[MoveId.MISTY_EXPLOSION]: ModifierTier.COMMON, [MoveId.MISTY_EXPLOSION]: RewardTier.COMMON,
[MoveId.GRASSY_GLIDE]: ModifierTier.COMMON, [MoveId.GRASSY_GLIDE]: RewardTier.COMMON,
[MoveId.RISING_VOLTAGE]: ModifierTier.COMMON, [MoveId.RISING_VOLTAGE]: RewardTier.COMMON,
[MoveId.TERRAIN_PULSE]: ModifierTier.COMMON, [MoveId.TERRAIN_PULSE]: RewardTier.COMMON,
[MoveId.SKITTER_SMACK]: ModifierTier.GREAT, [MoveId.SKITTER_SMACK]: RewardTier.GREAT,
[MoveId.BURNING_JEALOUSY]: ModifierTier.GREAT, [MoveId.BURNING_JEALOUSY]: RewardTier.GREAT,
[MoveId.LASH_OUT]: ModifierTier.GREAT, [MoveId.LASH_OUT]: RewardTier.GREAT,
[MoveId.POLTERGEIST]: ModifierTier.ULTRA, [MoveId.POLTERGEIST]: RewardTier.ULTRA,
[MoveId.CORROSIVE_GAS]: ModifierTier.COMMON, [MoveId.CORROSIVE_GAS]: RewardTier.COMMON,
[MoveId.COACHING]: ModifierTier.COMMON, [MoveId.COACHING]: RewardTier.COMMON,
[MoveId.FLIP_TURN]: ModifierTier.COMMON, [MoveId.FLIP_TURN]: RewardTier.COMMON,
[MoveId.TRIPLE_AXEL]: ModifierTier.COMMON, [MoveId.TRIPLE_AXEL]: RewardTier.COMMON,
[MoveId.DUAL_WINGBEAT]: ModifierTier.COMMON, [MoveId.DUAL_WINGBEAT]: RewardTier.COMMON,
[MoveId.SCORCHING_SANDS]: ModifierTier.GREAT, [MoveId.SCORCHING_SANDS]: RewardTier.GREAT,
[MoveId.TERA_BLAST]: ModifierTier.GREAT, [MoveId.TERA_BLAST]: RewardTier.GREAT,
[MoveId.ICE_SPINNER]: ModifierTier.GREAT, [MoveId.ICE_SPINNER]: RewardTier.GREAT,
[MoveId.SNOWSCAPE]: ModifierTier.COMMON, [MoveId.SNOWSCAPE]: RewardTier.COMMON,
[MoveId.POUNCE]: ModifierTier.COMMON, [MoveId.POUNCE]: RewardTier.COMMON,
[MoveId.TRAILBLAZE]: ModifierTier.COMMON, [MoveId.TRAILBLAZE]: RewardTier.COMMON,
[MoveId.CHILLING_WATER]: ModifierTier.COMMON, [MoveId.CHILLING_WATER]: RewardTier.COMMON,
[MoveId.HARD_PRESS]: ModifierTier.GREAT, [MoveId.HARD_PRESS]: RewardTier.GREAT,
[MoveId.DRAGON_CHEER]: ModifierTier.COMMON, [MoveId.DRAGON_CHEER]: RewardTier.COMMON,
[MoveId.ALLURING_VOICE]: ModifierTier.GREAT, [MoveId.ALLURING_VOICE]: RewardTier.GREAT,
[MoveId.TEMPER_FLARE]: ModifierTier.GREAT, [MoveId.TEMPER_FLARE]: RewardTier.GREAT,
[MoveId.SUPERCELL_SLAM]: ModifierTier.GREAT, [MoveId.SUPERCELL_SLAM]: RewardTier.GREAT,
[MoveId.PSYCHIC_NOISE]: ModifierTier.GREAT, [MoveId.PSYCHIC_NOISE]: RewardTier.GREAT,
[MoveId.UPPER_HAND]: ModifierTier.COMMON, [MoveId.UPPER_HAND]: RewardTier.COMMON,
}; };

View File

@ -22,7 +22,7 @@ import { TrainerType } from "#enums/trainer-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import { TypeColor, TypeShadow } from "#enums/color"; import { TypeColor, TypeShadow } from "#enums/color";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { pokemonFormChanges } from "./pokemon-forms"; import { pokemonFormChanges } from "./pokemon-forms";
import { pokemonEvolutions } from "./balance/pokemon-evolutions"; import { pokemonEvolutions } from "./balance/pokemon-evolutions";
@ -459,11 +459,11 @@ export class SingleGenerationChallenge extends Challenge {
.setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true)) .setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true))
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}); });
@ -476,12 +476,12 @@ export class SingleGenerationChallenge extends Challenge {
.setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true)) .setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true))
.setCustomModifierRewards({ .setCustomModifierRewards({
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}); });

View File

@ -1,5 +1,9 @@
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import type { HeldItem } from "#app/items/held-item";
import type { TrainerItem } from "#app/items/trainer-item";
import type { ModifierTypes } from "#app/modifier/modifier-type"; import type { ModifierTypes } from "#app/modifier/modifier-type";
import type { HeldItemId } from "#enums/held-item-id";
import type { TrainerItemId } from "#enums/trainer-item-id";
import type { Ability } from "./abilities/ability"; import type { Ability } from "./abilities/ability";
import type Move from "./moves/move"; import type Move from "./moves/move";
@ -7,5 +11,8 @@ export const allAbilities: Ability[] = [];
export const allMoves: Move[] = []; export const allMoves: Move[] = [];
export const allSpecies: PokemonSpecies[] = []; export const allSpecies: PokemonSpecies[] = [];
export const allHeldItems: Record<HeldItemId, HeldItem> = {};
export const allTrainerItems: Record<TrainerItemId, TrainerItem> = {};
// TODO: Figure out what this is used for and provide an appropriate tsdoc comment // TODO: Figure out what this is used for and provide an appropriate tsdoc comment
export const modifierTypes = {} as ModifierTypes; export const modifierTypes = {} as ModifierTypes;

View File

@ -36,15 +36,7 @@ import { ArenaTagSide } from "#enums/arena-tag-side";
import { import {
applyAbAttrs applyAbAttrs
} from "../abilities/apply-ab-attrs"; } from "../abilities/apply-ab-attrs";
import { allAbilities, allMoves } from "../data-lists"; import { allAbilities, allHeldItems, allMoves } from "../data-lists";
import {
AttackTypeBoosterModifier,
BerryModifier,
PokemonHeldItemModifier,
PokemonMoveAccuracyBoosterModifier,
PokemonMultiHitModifier,
PreserveBerryModifier,
} from "../../modifier/modifier";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { TerrainType } from "../terrain"; import { TerrainType } from "../terrain";
@ -86,10 +78,15 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MultiHitType } from "#enums/MultiHitType"; import { MultiHitType } from "#enums/MultiHitType";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves";
import { isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { HELD_ITEM_EFFECT } from "#app/items/held-item";
import { BerryHeldItem, berryTypeToHeldItem } from "#app/items/held-items/berry";
import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types";
import { applyMoveAttrs } from "./apply-attrs"; import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils"; import { frenzyMissFunc, getMoveTargets } from "./move-utils";
import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item";
import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability"; import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability";
import { applyHeldItems } from "#app/items/all-held-items";
/** /**
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}. * A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
@ -769,7 +766,7 @@ export default abstract class Move implements Localizable {
const isOhko = this.hasAttr("OneHitKOAccuracyAttr"); const isOhko = this.hasAttr("OneHitKOAccuracyAttr");
if (!isOhko) { if (!isOhko) {
globalScene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); applyHeldItems(HELD_ITEM_EFFECT.ACCURACY_BOOSTER, { pokemon: user, moveAccuracy: moveAccuracy });
} }
if (globalScene.arena.weather?.weatherType === WeatherType.FOG) { if (globalScene.arena.weather?.weatherType === WeatherType.FOG) {
@ -806,7 +803,7 @@ export default abstract class Move implements Localizable {
applyAbAttrs("MoveTypeChangeAbAttr", {pokemon: source, opponent: target, move: this, simulated: true, moveType: typeChangeHolder, power: typeChangeMovePowerMultiplier}); applyAbAttrs("MoveTypeChangeAbAttr", {pokemon: source, opponent: target, move: this, simulated: true, moveType: typeChangeHolder, power: typeChangeMovePowerMultiplier});
const sourceTeraType = source.getTeraType(); const sourceTeraType = source.getTeraType();
if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) { if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !source.heldItemManager.hasItem(HeldItemId.MULTI_LENS)) {
power.value = 60; power.value = 60;
} }
@ -851,7 +848,11 @@ export default abstract class Move implements Localizable {
if (!this.hasAttr("TypelessAttr")) { if (!this.hasAttr("TypelessAttr")) {
globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power); globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power);
globalScene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, typeChangeHolder.value, power); applyHeldItems(HELD_ITEM_EFFECT.ATTACK_TYPE_BOOST, {
pokemon: source,
moveType: typeChangeHolder.value,
movePower: power,
});
} }
if (source.getTag(HelpingHandTag)) { if (source.getTag(HelpingHandTag)) {
@ -914,7 +915,7 @@ export default abstract class Move implements Localizable {
* Returns `true` if this move can be given additional strikes * Returns `true` if this move can be given additional strikes
* by enhancing effects. * by enhancing effects.
* Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond} * Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond}
* and {@linkcode PokemonMultiHitModifier | Multi-Lens}. * and {@linkcode MultiHitHeldItem | Multi-Lens}.
* @param user The {@linkcode Pokemon} using the move * @param user The {@linkcode Pokemon} using the move
* @param restrictSpread `true` if the enhancing effect * @param restrictSpread `true` if the enhancing effect
* should not affect multi-target moves (default `false`) * should not affect multi-target moves (default `false`)
@ -1565,7 +1566,7 @@ export class TargetHalfHpDamageAttr extends FixedDamageAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// first, determine if the hit is coming from multi lens or not // first, determine if the hit is coming from multi lens or not
const lensCount = user.getHeldItems().find(i => i instanceof PokemonMultiHitModifier)?.getStackCount() ?? 0; const lensCount = user.heldItemManager.getStack(HeldItemId.MULTI_LENS);
if (lensCount <= 0) { if (lensCount <= 0) {
// no multi lenses; we can just halve the target's hp and call it a day // no multi lenses; we can just halve the target's hp and call it a day
(args[0] as NumberHolder).value = toDmgValue(target.hp / 2); (args[0] as NumberHolder).value = toDmgValue(target.hp / 2);
@ -2621,35 +2622,33 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
return false; return false;
} }
const heldItems = this.getTargetHeldItems(target).filter((i) => i.isTransferable); const heldItems = target.heldItemManager.getTransferableHeldItems();
if (!heldItems.length) { if (!heldItems.length) {
return false; return false;
} }
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD; const stolenItem = heldItems[user.randBattleSeedInt(heldItems.length)];
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); if (!globalScene.tryTransferHeldItem(stolenItem, target, user, false)) {
const stolenItem = tierHeldItems[user.randBattleSeedInt(tierHeldItems.length)];
if (!globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) {
return false; return false;
} }
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:stoleItem",
{ pokemonName: getPokemonNameWithAffix(user),
targetName: getPokemonNameWithAffix(target),
itemName: allHeldItems[stolenItem].name
}
));
return true; return true;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[];
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target); const heldItems = target.heldItemManager.getTransferableHeldItems();
return heldItems.length ? 5 : 0; return heldItems.length ? 5 : 0;
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target); const heldItems = target.heldItemManager.getTransferableHeldItems();
return heldItems.length ? -5 : 0; return heldItems.length ? -5 : 0;
} }
} }
@ -2695,10 +2694,10 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
// Considers entire transferrable item pool by default (Knock Off). // Considers entire transferrable item pool by default (Knock Off).
// Otherwise only consider berries (Incinerate). // Otherwise only consider berries (Incinerate).
let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); let heldItems = target.heldItemManager.getTransferableHeldItems();
if (this.berriesOnly) { if (this.berriesOnly) {
heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer()); heldItems = heldItems.filter(m => m in Object.values(berryTypeToHeldItem));
} }
if (!heldItems.length) { if (!heldItems.length) {
@ -2709,29 +2708,26 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
// Decrease item amount and update icon // Decrease item amount and update icon
target.loseHeldItem(removedItem); target.loseHeldItem(removedItem);
globalScene.updateModifiers(target.isPlayer()); globalScene.updateItems(target.isPlayer());
if (this.berriesOnly) { if (this.berriesOnly) {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:incineratedItem",
{ pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: allHeldItems[removedItem].name }));
} else { } else {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:knockedOffItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:knockedOffItem",
{ pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: allHeldItems[removedItem].name }));
} }
return true; return true;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[];
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target); const heldItems = target.getHeldItems();
return heldItems.length ? 5 : 0; return heldItems.length ? 5 : 0;
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target); const heldItems = target.getHeldItems();
return heldItems.length ? -5 : 0; return heldItems.length ? -5 : 0;
} }
} }
@ -2740,7 +2736,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
* Attribute that causes targets of the move to eat a berry. Used for Teatime, Stuff Cheeks * Attribute that causes targets of the move to eat a berry. Used for Teatime, Stuff Cheeks
*/ */
export class EatBerryAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr {
protected chosenBerry: BerryModifier; protected chosenBerry: HeldItemId;
constructor(selfTarget: boolean) { constructor(selfTarget: boolean) {
super(selfTarget); super(selfTarget);
} }
@ -2769,7 +2765,7 @@ export class EatBerryAttr extends MoveEffectAttr {
this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)];
const preserve = new BooleanHolder(false); const preserve = new BooleanHolder(false);
// check for berry pouch preservation // check for berry pouch preservation
globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.PRESERVE_BERRY, {pokemon: pokemon, doPreserve: preserve});
if (!preserve.value) { if (!preserve.value) {
this.reduceBerryModifier(pokemon); this.reduceBerryModifier(pokemon);
} }
@ -2780,16 +2776,15 @@ export class EatBerryAttr extends MoveEffectAttr {
return true; return true;
} }
getTargetHeldBerries(target: Pokemon): BerryModifier[] { getTargetHeldBerries(target: Pokemon): HeldItemId[] {
return globalScene.findModifiers(m => m instanceof BerryModifier return target.getHeldItems().filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY));
&& (m as BerryModifier).pokemonId === target.id, target.isPlayer()) as BerryModifier[];
} }
reduceBerryModifier(target: Pokemon) { reduceBerryModifier(target: Pokemon) {
if (this.chosenBerry) { if (this.chosenBerry) {
target.loseHeldItem(this.chosenBerry); target.loseHeldItem(this.chosenBerry);
} }
globalScene.updateModifiers(target.isPlayer()); globalScene.updateItems(target.isPlayer());
} }
@ -2803,10 +2798,10 @@ export class EatBerryAttr extends MoveEffectAttr {
*/ */
protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) { protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) {
// consumer eats berry, owner triggers unburden and similar effects // consumer eats berry, owner triggers unburden and similar effects
getBerryEffectFunc(this.chosenBerry.berryType)(consumer); getBerryEffectFunc((allHeldItems[this.chosenBerry] as BerryHeldItem).berryType)(consumer);
applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner}); applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner});
applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer}); applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer});
consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest); consumer.recordEatenBerry((allHeldItems[this.chosenBerry] as BerryHeldItem).berryType, updateHarvest);
} }
} }
@ -2845,7 +2840,7 @@ export class StealEatBerryAttr extends EatBerryAttr {
// pick a random berry and eat it // pick a random berry and eat it
this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)];
applyAbAttrs("PostItemLostAbAttr", {pokemon: target}); applyAbAttrs("PostItemLostAbAttr", {pokemon: target});
const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: allHeldItems[this.chosenBerry].name });
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
this.reduceBerryModifier(target); this.reduceBerryModifier(target);
this.eatBerry(user, target); this.eatBerry(user, target);
@ -6424,9 +6419,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
} }
// clear out enemy held item modifiers of the switch out target
globalScene.clearEnemyHeldItemModifiers(switchOutTarget);
if (!allyPokemon?.isActive(true) && switchOutTarget.hp) { if (!allyPokemon?.isActive(true) && switchOutTarget.hp) {
globalScene.phaseManager.pushNew("BattleEndPhase", false); globalScene.phaseManager.pushNew("BattleEndPhase", false);
@ -8017,14 +8009,14 @@ const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Po
const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => !target.isOfType(PokemonType.GHOST); const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => !target.isOfType(PokemonType.GHOST);
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0; const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.heldItemManager.getTransferableHeldItems().length > 0;
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => { const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const heldItems = target.getHeldItems().filter(i => i.isTransferable); const heldItems = target.heldItemManager.getTransferableHeldItems();
if (heldItems.length === 0) { if (heldItems.length === 0) {
return ""; return "";
} }
const itemName = heldItems[0]?.type?.name ?? "item"; const itemName = allHeldItems[heldItems[0]].name ?? "item";
const message: string = i18next.t("moveTriggers:attackedByItem", { pokemonName: getPokemonNameWithAffix(target), itemName: itemName }); const message: string = i18next.t("moveTriggers:attackedByItem", { pokemonName: getPokemonNameWithAffix(target), itemName: itemName });
return message; return message;
}; };
@ -9319,7 +9311,7 @@ export function initMoves() {
.condition((user, target, move) => !target.status && !target.isSafeguarded(user)) .condition((user, target, move) => !target.status && !target.isSafeguarded(user))
.reflectable(), .reflectable(),
new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.heldItemManager.getTransferableHeldItems().length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false) .attr(RemoveHeldItemAttr, false)
.edgeCase(), .edgeCase(),
// Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc. // Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc.
@ -10041,7 +10033,7 @@ export function initMoves() {
.condition((user, target, move) => !target.turnData.acted) .condition((user, target, move) => !target.turnData.acted)
.attr(ForceLastAttr), .attr(ForceLastAttr),
new AttackMove(MoveId.ACROBATICS, PokemonType.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) new AttackMove(MoveId.ACROBATICS, PokemonType.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.heldItemManager.getTransferableHeldItems().reduce((v, m) => v + user.heldItemManager.getStack(m), 0))),
new StatusMove(MoveId.REFLECT_TYPE, PokemonType.NORMAL, -1, 15, -1, 0, 5) new StatusMove(MoveId.REFLECT_TYPE, PokemonType.NORMAL, -1, 15, -1, 0, 5)
.ignoresSubstitute() .ignoresSubstitute()
.attr(CopyTypeAttr), .attr(CopyTypeAttr),
@ -10790,7 +10782,7 @@ export function initMoves() {
.attr(EatBerryAttr, true) .attr(EatBerryAttr, true)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
.condition((user) => { .condition((user) => {
const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()); const userBerries = user.getHeldItems().filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY));
return userBerries.length > 0; return userBerries.length > 0;
}) })
.edgeCase(), // Stuff Cheeks should not be selectable when the user does not have a berry, see wiki .edgeCase(), // Stuff Cheeks should not be selectable when the user does not have a berry, see wiki

View File

@ -19,7 +19,7 @@ import i18next from "i18next";
import type { IEggOptions } from "#app/data/egg"; import type { IEggOptions } from "#app/data/egg";
import { EggSourceType } from "#enums/egg-source-types"; import { EggSourceType } from "#enums/egg-source-types";
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
@ -165,7 +165,7 @@ export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder.
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTypeFuncs: [modifierTypes.SACRED_ASH], guaranteedModifierTypeFuncs: [modifierTypes.SACRED_ASH],
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA], guaranteedModifierTiers: [RewardTier.ROGUE, RewardTier.ULTRA],
fillRemaining: true, fillRemaining: true,
}, },
[eggOptions], [eggOptions],

View File

@ -1,6 +1,6 @@
import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
generateModifierType, getPartyBerries,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterRewards, setEncounterRewards,
@ -9,40 +9,47 @@ import {
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { EnemyPokemon } from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import type { BerryModifierType, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { BerryModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { randInt } from "#app/utils/common"; import { randInt } from "#app/utils/common";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { import { catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
applyModifierTypeToPlayerPokemon,
catchPokemon,
getHighestLevelPlayerPokemon,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import type HeldModifierConfig from "#app/@types/held-modifier-config";
import type { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import i18next from "i18next"; import i18next from "i18next";
import type { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import type { HeldItemConfiguration } from "#app/items/held-item-data-types";
import { allHeldItems } from "#app/data/data-lists";
import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { HeldItemRequirement } from "../mystery-encounter-requirements";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/absoluteAvarice"; const namespace = "mysteryEncounters/absoluteAvarice";
function berrySprite(spriteKey: string, x: number, y: number): MysteryEncounterSpriteConfig {
return {
spriteKey: spriteKey,
fileRoot: "items",
isItem: true,
x: x,
y: y,
hidden: true,
disableAnimation: true,
};
}
/** /**
* Absolute Avarice encounter. * Absolute Avarice encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805} * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805}
@ -53,7 +60,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
) )
.withEncounterTier(MysteryEncounterTier.GREAT) .withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(20, 180) .withSceneWaveRangeRequirement(20, 180)
.withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 6)) // Must have at least 6 berries to spawn .withSceneRequirement(new HeldItemRequirement(HeldItemCategoryId.BERRY, 6)) // Must have at least 6 berries to spawn
.withFleeAllowed(false) .withFleeAllowed(false)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
@ -74,105 +81,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
repeat: true, repeat: true,
x: -5, x: -5,
}, },
{ berrySprite("lum_berry", 7, -14),
spriteKey: "lum_berry", berrySprite("salac_berry", 2, 4),
fileRoot: "items", berrySprite("lansat_berry", 32, 5),
isItem: true, berrySprite("liechi_berry", 6, -5),
x: 7, berrySprite("sitrus_berry", 7, 8),
y: -14, berrySprite("enigma_berry", 26, -4),
hidden: true, berrySprite("leppa_berry", 16, -27),
disableAnimation: true, berrySprite("petaya_berry", 30, -17),
}, berrySprite("ganlon_berry", 16, -11),
{ berrySprite("apicot_berry", 14, -2),
spriteKey: "salac_berry", berrySprite("starf_berry", 18, 9),
fileRoot: "items",
isItem: true,
x: 2,
y: 4,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "lansat_berry",
fileRoot: "items",
isItem: true,
x: 32,
y: 5,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "liechi_berry",
fileRoot: "items",
isItem: true,
x: 6,
y: -5,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "sitrus_berry",
fileRoot: "items",
isItem: true,
x: 7,
y: 8,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "enigma_berry",
fileRoot: "items",
isItem: true,
x: 26,
y: -4,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "leppa_berry",
fileRoot: "items",
isItem: true,
x: 16,
y: -27,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "petaya_berry",
fileRoot: "items",
isItem: true,
x: 30,
y: -17,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "ganlon_berry",
fileRoot: "items",
isItem: true,
x: 16,
y: -11,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "apicot_berry",
fileRoot: "items",
isItem: true,
x: 14,
y: -2,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "starf_berry",
fileRoot: "items",
isItem: true,
x: 18,
y: 9,
hidden: true,
disableAnimation: true,
},
]) ])
.withHideWildIntroMessage(true) .withHideWildIntroMessage(true)
.withAutoHideIntroVisuals(false) .withAutoHideIntroVisuals(false)
@ -191,35 +110,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
globalScene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); globalScene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav");
globalScene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); globalScene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3");
// Get all player berry items, remove from party, and store reference // Get all berries in party, with references to the pokemon
const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; const berryItems = getPartyBerries();
// Sort berries by party member ID to more easily re-add later if necessary encounter.misc.berryItemsMap = berryItems;
const berryItemsMap = new Map<number, BerryModifier[]>();
globalScene.getPlayerParty().forEach(pokemon => { // Adds stolen berries to the Greedent item configuration
const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); const bossHeldItemConfig: HeldItemConfiguration = [];
if (pokemonBerries?.length > 0) { berryItems.forEach(map => {
berryItemsMap.set(pokemon.id, pokemonBerries); bossHeldItemConfig.push({ entry: map.item, count: 1 });
}
}); });
encounter.misc = { berryItemsMap };
// Generates copies of the stolen berries to put on the Greedent
const bossModifierConfigs: HeldModifierConfig[] = [];
berryItems.forEach(berryMod => {
// Can't define stack count on a ModifierType, have to just create separate instances for each stack
// Overflow berries will be "lost" on the boss, but it's un-catchable anyway
for (let i = 0; i < berryMod.stackCount; i++) {
const modifierType = generateModifierType(modifierTypes.BERRY, [
berryMod.berryType,
]) as PokemonHeldItemModifierType;
bossModifierConfigs.push({ modifier: modifierType });
}
});
// Do NOT remove the real berries yet or else it will be persisted in the session data
// +1 SpDef below wave 50, SpDef and Speed otherwise // +1 SpDef below wave 50, SpDef and Speed otherwise
const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] = const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] =
globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.SPDEF, Stat.SPD]; globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.SPDEF, Stat.SPD];
@ -234,7 +135,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
bossSegments: 3, bossSegments: 3,
shiny: false, // Shiny lock because of consistency issues between the different options shiny: false, // Shiny lock because of consistency issues between the different options
moveSet: [MoveId.THRASH, MoveId.CRUNCH, MoveId.BODY_PRESS, MoveId.SLACK_OFF], moveSet: [MoveId.THRASH, MoveId.CRUNCH, MoveId.BODY_PRESS, MoveId.SLACK_OFF],
modifierConfigs: bossModifierConfigs, heldItemConfig: bossHeldItemConfig,
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => { mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(`${namespace}:option.1.boss_enraged`); queueEncounterMessage(`${namespace}:option.1.boss_enraged`);
@ -261,12 +162,12 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
// Remove the berries from the party // Remove the berries from the party
// Session has been safely saved at this point, so data won't be lost // Session has been safely saved at this point, so data won't be lost
const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; const berryItems = getPartyBerries();
berryItems.forEach(berryMod => { berryItems.forEach(map => {
globalScene.removeModifier(berryMod); globalScene.getPokemonById(map.pokemonId)?.heldItemManager.remove(map.item.id);
}); });
globalScene.updateModifiers(true); globalScene.updateItems(true);
return true; return true;
}) })
@ -286,19 +187,14 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
// Provides 1x Reviver Seed to each party member at end of battle // Provides 1x Reviver Seed to each party member at end of battle
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED);
encounter.setDialogueToken( encounter.setDialogueToken(
"foodReward", "foodReward",
revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"), allHeldItems[HeldItemId.REVIVER_SEED].name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"),
); );
const givePartyPokemonReviverSeeds = () => { const givePartyPokemonReviverSeeds = () => {
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
party.forEach(p => { party.forEach(p => {
const heldItems = p.getHeldItems(); p.heldItemManager.add(HeldItemId.REVIVER_SEED);
if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) {
const seedModifier = revSeed.newModifier(p);
globalScene.addModifier(seedModifier, false, false, false, true);
}
}); });
queueEncounterMessage(`${namespace}:option.1.food_stash`); queueEncounterMessage(`${namespace}:option.1.food_stash`);
}; };
@ -334,23 +230,20 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
// Returns 2/5 of the berries stolen to each Pokemon // Returns 2/5 of the berries stolen to each Pokemon
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
party.forEach(pokemon => { party.forEach(pokemon => {
const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); // TODO: is this check legal?
const berryTypesAsArray: BerryType[] = []; const stolenBerries = berryMap.filter(map => map.pokemon === pokemon);
stolenBerries?.forEach(bMod => berryTypesAsArray.push(...new Array(bMod.stackCount).fill(bMod.berryType))); const returnedBerryCount = Math.floor(((stolenBerries.length ?? 0) * 2) / 5);
const returnedBerryCount = Math.floor(((berryTypesAsArray.length ?? 0) * 2) / 5);
if (returnedBerryCount > 0) { if (returnedBerryCount > 0) {
for (let i = 0; i < returnedBerryCount; i++) { for (let i = 0; i < returnedBerryCount; i++) {
// Shuffle remaining berry types and pop // Shuffle remaining berry types and pop
Phaser.Math.RND.shuffle(berryTypesAsArray); Phaser.Math.RND.shuffle(stolenBerries);
const randBerryType = berryTypesAsArray.pop(); const randBerryType = stolenBerries.pop();
pokemon.heldItemManager.add(randBerryType);
const berryModType = generateModifierType(modifierTypes.BERRY, [randBerryType]) as BerryModifierType;
applyModifierTypeToPlayerPokemon(pokemon, berryModType);
} }
} }
}); });
await globalScene.updateModifiers(true); await globalScene.updateItems(true);
await transitionMysteryEncounterIntroVisuals(true, true, 500); await transitionMysteryEncounterIntroVisuals(true, true, 500);
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);

View File

@ -1,5 +1,4 @@
import { import {
generateModifierType,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterExp, setEncounterExp,
updatePlayerMoney, updatePlayerMoney,
@ -24,6 +23,8 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import i18next from "i18next"; import i18next from "i18next";
import { allTrainerItems } from "#app/data/data-lists";
import { TrainerItemId } from "#enums/trainer-item-id";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/anOfferYouCantRefuse"; const namespace = "mysteryEncounters/anOfferYouCantRefuse";
@ -109,8 +110,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB
} }
} }
const shinyCharm = generateModifierType(modifierTypes.SHINY_CHARM); const name = allTrainerItems[TrainerItemId.SHINY_CHARM].name;
encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name")); encounter.setDialogueToken("itemName", name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name"));
encounter.setDialogueToken("liepardName", getPokemonSpecies(SpeciesId.LIEPARD).getName()); encounter.setDialogueToken("liepardName", getPokemonSpecies(SpeciesId.LIEPARD).getName());
return true; return true;

View File

@ -1,7 +1,6 @@
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
generateModifierType,
generateModifierTypeOption, generateModifierTypeOption,
getRandomEncounterSpecies, getRandomEncounterSpecies,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
@ -11,7 +10,7 @@ import {
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import type { BerryModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; import type { ModifierTypeOption } from "#app/modifier/modifier-type";
import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
@ -26,18 +25,17 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { import {
applyModifierTypeToPlayerPokemon,
getEncounterPokemonLevelForWave, getEncounterPokemonLevelForWave,
getHighestStatPlayerPokemon, getHighestStatPlayerPokemon,
getSpriteKeysFromPokemon, getSpriteKeysFromPokemon,
STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import PokemonData from "#app/system/pokemon-data"; import PokemonData from "#app/system/pokemon-data";
import { BerryModifier } from "#app/modifier/modifier";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { PERMANENT_STATS, Stat } from "#enums/stat"; import { PERMANENT_STATS, Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { berryTypeToHeldItem } from "#app/items/held-items/berry";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/berriesAbound"; const namespace = "mysteryEncounters/berriesAbound";
@ -312,35 +310,17 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
function tryGiveBerry(prioritizedPokemon?: PlayerPokemon) { function tryGiveBerry(prioritizedPokemon?: PlayerPokemon) {
const berryType = randSeedInt(Object.keys(BerryType).filter(s => !Number.isNaN(Number(s))).length) as BerryType; const berryType = randSeedInt(Object.keys(BerryType).filter(s => !Number.isNaN(Number(s))).length) as BerryType;
const berry = generateModifierType(modifierTypes.BERRY, [berryType]) as BerryModifierType; const berry = berryTypeToHeldItem[berryType];
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
// Will try to apply to prioritized pokemon first, then do normal application method if it fails // Will give the berry to a Pokemon, starting from the prioritized one
if (prioritizedPokemon) { if (prioritizedPokemon?.heldItemManager.add(berry)) {
const heldBerriesOfType = globalScene.findModifier( return;
m =>
m instanceof BerryModifier &&
m.pokemonId === prioritizedPokemon.id &&
(m as BerryModifier).berryType === berryType,
true,
) as BerryModifier;
if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount()) {
applyModifierTypeToPlayerPokemon(prioritizedPokemon, berry);
return;
}
} }
// Iterate over the party until berry was successfully given
for (const pokemon of party) { for (const pokemon of party) {
const heldBerriesOfType = globalScene.findModifier( if (pokemon.heldItemManager.add(berry)) {
m => m instanceof BerryModifier && m.pokemonId === pokemon.id && (m as BerryModifier).berryType === berryType,
true,
) as BerryModifier;
if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount()) {
applyModifierTypeToPlayerPokemon(pokemon, berry);
return; return;
} }
} }

View File

@ -1,6 +1,5 @@
import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
generateModifierType,
generateModifierTypeOption, generateModifierTypeOption,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
@ -31,28 +30,22 @@ import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { import {
AttackTypeBoosterHeldItemTypeRequirement,
CombinationPokemonRequirement, CombinationPokemonRequirement,
HeldItemRequirement, HeldItemRequirement,
TypeRequirement, TypeRequirement,
} from "#app/data/mystery-encounters/mystery-encounter-requirements"; } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import type { AttackTypeBoosterModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; import type { ModifierTypeOption } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
import {
AttackTypeBoosterModifier,
BypassSpeedChanceModifier,
ContactHeldItemTransferChanceModifier,
GigantamaxAccessModifier,
MegaEvolutionAccessModifier,
} from "#app/modifier/modifier";
import i18next from "i18next"; import i18next from "i18next";
import MoveInfoOverlay from "#app/ui/move-info-overlay"; import MoveInfoOverlay from "#app/ui/move-info-overlay";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { HeldItemId } from "#enums/held-item-id";
import { allHeldItems } from "#app/data/data-lists";
import { TrainerItemId } from "#enums/trainer-item-id";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/bugTypeSuperfan"; const namespace = "mysteryEncounters/bugTypeSuperfan";
@ -143,6 +136,8 @@ const POOL_3_POKEMON: { species: SpeciesId; formIndex?: number }[] = [
const POOL_4_POKEMON = [SpeciesId.GENESECT, SpeciesId.SLITHER_WING, SpeciesId.BUZZWOLE, SpeciesId.PHEROMOSA]; const POOL_4_POKEMON = [SpeciesId.GENESECT, SpeciesId.SLITHER_WING, SpeciesId.BUZZWOLE, SpeciesId.PHEROMOSA];
const REQUIRED_ITEMS = [HeldItemId.QUICK_CLAW, HeldItemId.GRIP_CLAW, HeldItemId.SILVER_POWDER];
const PHYSICAL_TUTOR_MOVES = [ const PHYSICAL_TUTOR_MOVES = [
MoveId.MEGAHORN, MoveId.MEGAHORN,
MoveId.ATTACK_ORDER, MoveId.ATTACK_ORDER,
@ -186,8 +181,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
.withPrimaryPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some( CombinationPokemonRequirement.Some(
// Must have at least 1 Bug type on team, OR have a bug item somewhere on the team // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team
new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), new HeldItemRequirement(REQUIRED_ITEMS, 1),
new AttackTypeBoosterHeldItemTypeRequirement(PokemonType.BUG, 1),
new TypeRequirement(PokemonType.BUG, false, 1), new TypeRequirement(PokemonType.BUG, false, 1),
), ),
) )
@ -259,13 +253,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
}, },
]; ];
const requiredItems = [ const requiredItemString = REQUIRED_ITEMS.map(m => allHeldItems[m].name ?? "unknown").join("/");
generateModifierType(modifierTypes.QUICK_CLAW),
generateModifierType(modifierTypes.GRIP_CLAW),
generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [PokemonType.BUG]),
];
const requiredItemString = requiredItems.map(m => m?.name ?? "unknown").join("/");
encounter.setDialogueToken("requiredBugItems", requiredItemString); encounter.setDialogueToken("requiredBugItems", requiredItemString);
return true; return true;
@ -366,10 +354,10 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
const modifierOptions: ModifierTypeOption[] = [generateModifierTypeOption(modifierTypes.MASTER_BALL)!]; const modifierOptions: ModifierTypeOption[] = [generateModifierTypeOption(modifierTypes.MASTER_BALL)!];
const specialOptions: ModifierTypeOption[] = []; const specialOptions: ModifierTypeOption[] = [];
if (!globalScene.findModifier(m => m instanceof MegaEvolutionAccessModifier)) { if (!globalScene.trainerItems.hasItem(TrainerItemId.MEGA_BRACELET)) {
modifierOptions.push(generateModifierTypeOption(modifierTypes.MEGA_BRACELET)!); modifierOptions.push(generateModifierTypeOption(modifierTypes.MEGA_BRACELET)!);
} }
if (!globalScene.findModifier(m => m instanceof GigantamaxAccessModifier)) { if (!globalScene.trainerItems.hasItem(TrainerItemId.DYNAMAX_BAND)) {
modifierOptions.push(generateModifierTypeOption(modifierTypes.DYNAMAX_BAND)!); modifierOptions.push(generateModifierTypeOption(modifierTypes.DYNAMAX_BAND)!);
} }
const nonRareEvolutionModifier = generateModifierTypeOption(modifierTypes.EVOLUTION_ITEM); const nonRareEvolutionModifier = generateModifierTypeOption(modifierTypes.EVOLUTION_ITEM);
@ -415,8 +403,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
.withPrimaryPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some( CombinationPokemonRequirement.Some(
// Meets one or both of the below reqs // Meets one or both of the below reqs
new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), new HeldItemRequirement(REQUIRED_ITEMS, 1),
new AttackTypeBoosterHeldItemTypeRequirement(PokemonType.BUG, 1),
), ),
) )
.withDialogue({ .withDialogue({
@ -439,25 +426,19 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(item => { const validItems = pokemon.heldItemManager.getTransferableHeldItems().filter(item => {
return ( item in REQUIRED_ITEMS;
(item instanceof BypassSpeedChanceModifier ||
item instanceof ContactHeldItemTransferChanceModifier ||
(item instanceof AttackTypeBoosterModifier &&
(item.type as AttackTypeBoosterModifierType).moveType === PokemonType.BUG)) &&
item.isTransferable
);
}); });
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((item: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: modifier.type.name, label: allHeldItems[item].name,
handler: () => { handler: () => {
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("selectedItem", modifier.type.name); encounter.setDialogueToken("selectedItem", allHeldItems[item].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
chosenModifier: modifier, chosenItem: item,
}; };
return true; return true;
}, },
@ -469,12 +450,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon has valid item, it can be selected // If pokemon has valid item, it can be selected
const hasValidItem = pokemon.getHeldItems().some(item => { const hasValidItem = pokemon.getHeldItems().some(item => {
return ( item in REQUIRED_ITEMS;
item instanceof BypassSpeedChanceModifier ||
item instanceof ContactHeldItemTransferChanceModifier ||
(item instanceof AttackTypeBoosterModifier &&
(item.type as AttackTypeBoosterModifierType).moveType === PokemonType.BUG)
);
}); });
if (!hasValidItem) { if (!hasValidItem) {
return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null;
@ -491,10 +467,10 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
chosenPokemon.loseHeldItem(modifier, false); chosenPokemon.loseHeldItem(modifier, false);
globalScene.updateModifiers(true, true); globalScene.updateItems(true);
const bugNet = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; const bugNet = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!;
bugNet.type.tier = ModifierTier.ROGUE; bugNet.type.tier = RewardTier.ROGUE;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: [bugNet], guaranteedModifierTypeOptions: [bugNet],

View File

@ -1,6 +1,5 @@
import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
loadCustomMovesForEncounter, loadCustomMovesForEncounter,
@ -11,9 +10,7 @@ import {
import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { trainerConfigs } from "#app/data/trainers/trainer-config";
import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate";
import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { ModifierPoolType } from "#enums/modifier-pool-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
@ -24,10 +21,7 @@ import { SpeciesId } from "#enums/species-id";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { import { applyAbilityOverrideToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
applyAbilityOverrideToPokemon,
applyModifierTypeToPlayerPokemon,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -38,8 +32,6 @@ import i18next from "i18next";
import type { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; import type { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import { BerryModifier } from "#app/modifier/modifier";
import { BerryType } from "#enums/berry-type";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { EncounterBattleAnim } from "#app/data/battle-anims"; import { EncounterBattleAnim } from "#app/data/battle-anims";
@ -49,7 +41,10 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { allAbilities, modifierTypes } from "#app/data/data-lists"; import { allAbilities } from "#app/data/data-lists";
import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { getHeldItemTier } from "#app/items/held-item-tiers";
import { assignItemsFromConfiguration } from "#app/items/held-item-pool";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/clowningAround"; const namespace = "mysteryEncounters/clowningAround";
@ -283,16 +278,16 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
let mostHeldItemsPokemon = party[0]; let mostHeldItemsPokemon = party[0];
let count = mostHeldItemsPokemon let count = mostHeldItemsPokemon.heldItemManager
.getHeldItems() .getTransferableHeldItems()
.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY))
.reduce((v, m) => v + m.stackCount, 0); .reduce((v, m) => v + mostHeldItemsPokemon.heldItemManager.getStack(m), 0);
for (const pokemon of party) { for (const pokemon of party) {
const nextCount = pokemon const nextCount = pokemon.heldItemManager
.getHeldItems() .getTransferableHeldItems()
.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY))
.reduce((v, m) => v + m.stackCount, 0); .reduce((v, m) => v + pokemon.heldItemManager.getStack(m), 0);
if (nextCount > count) { if (nextCount > count) {
mostHeldItemsPokemon = pokemon; mostHeldItemsPokemon = pokemon;
count = nextCount; count = nextCount;
@ -301,16 +296,31 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender()); encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender());
const items = mostHeldItemsPokemon.getHeldItems(); const items = mostHeldItemsPokemon.heldItemManager
.getTransferableHeldItems()
.filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY));
// Shuffles Berries (if they have any) // Shuffles Berries (if they have any)
const oldBerries = mostHeldItemsPokemon.heldItemManager
.getHeldItems()
.filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY));
let numBerries = 0; let numBerries = 0;
for (const m of items.filter(m => m instanceof BerryModifier)) { for (const berry of oldBerries) {
numBerries += m.stackCount; const stack = mostHeldItemsPokemon.heldItemManager.getStack(berry);
globalScene.removeModifier(m); numBerries += stack;
mostHeldItemsPokemon.heldItemManager.remove(berry, stack);
} }
generateItemsOfTier(mostHeldItemsPokemon, numBerries, "Berries"); assignItemsFromConfiguration(
[
{
entry: HeldItemCategoryId.BERRY,
count: numBerries,
},
],
mostHeldItemsPokemon,
);
// Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm)
// For the purpose of this ME, Soothe Bells and Lucky Eggs are counted as Ultra tier // For the purpose of this ME, Soothe Bells and Lucky Eggs are counted as Ultra tier
@ -318,20 +328,36 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
let numUltra = 0; let numUltra = 0;
let numRogue = 0; let numRogue = 0;
for (const m of items.filter(m => m.isTransferable && !(m instanceof BerryModifier))) { for (const m of items) {
const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party); const tier = getHeldItemTier(m) ?? RewardTier.ULTRA;
const tier = type.tier ?? ModifierTier.ULTRA; const stack = mostHeldItemsPokemon.heldItemManager.getStack(m);
if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { if (tier === RewardTier.ROGUE) {
numRogue += m.stackCount; numRogue += stack;
globalScene.removeModifier(m); } else if (tier === RewardTier.ULTRA) {
} else if (type.id === "LUCKY_EGG" || type.id === "SOOTHE_BELL" || tier === ModifierTier.ULTRA) { numUltra += stack;
numUltra += m.stackCount;
globalScene.removeModifier(m);
} }
mostHeldItemsPokemon.heldItemManager.remove(m, stack);
} }
generateItemsOfTier(mostHeldItemsPokemon, numUltra, ModifierTier.ULTRA); assignItemsFromConfiguration(
generateItemsOfTier(mostHeldItemsPokemon, numRogue, ModifierTier.ROGUE); [
{
entry: ultraPool,
count: numUltra,
},
],
mostHeldItemsPokemon,
);
assignItemsFromConfiguration(
[
{
entry: roguePool,
count: numRogue,
},
],
mostHeldItemsPokemon,
);
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
@ -487,68 +513,21 @@ function onYesAbilitySwap(resolve) {
selectPokemonForOption(onPokemonSelected, onPokemonNotSelected); selectPokemonForOption(onPokemonSelected, onPokemonNotSelected);
} }
function generateItemsOfTier(pokemon: PlayerPokemon, numItems: number, tier: ModifierTier | "Berries") { const ultraPool = [
// These pools have to be defined at runtime so that modifierTypes exist { entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 1 },
// Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon { entry: HeldItemId.REVIVER_SEED, weight: 1 },
// This is to prevent "over-generating" a random item of a certain type during item swaps { entry: HeldItemId.GOLDEN_PUNCH, weight: 1 },
const ultraPool = [ { entry: HeldItemId.QUICK_CLAW, weight: 1 },
[modifierTypes.REVIVER_SEED, 1], { entry: HeldItemId.WIDE_LENS, weight: 1 },
[modifierTypes.GOLDEN_PUNCH, 5], ];
[modifierTypes.ATTACK_TYPE_BOOSTER, 99],
[modifierTypes.QUICK_CLAW, 3],
[modifierTypes.WIDE_LENS, 3],
];
const roguePool = [ const roguePool = [
[modifierTypes.LEFTOVERS, 4], { entry: HeldItemId.LEFTOVERS, weight: 1 },
[modifierTypes.SHELL_BELL, 4], { entry: HeldItemId.SHELL_BELL, weight: 1 },
[modifierTypes.SOUL_DEW, 10], { entry: HeldItemId.SOUL_DEW, weight: 1 },
[modifierTypes.SCOPE_LENS, 1], { entry: HeldItemId.SCOPE_LENS, weight: 1 },
[modifierTypes.BATON, 1], { entry: HeldItemId.BATON, weight: 1 },
[modifierTypes.FOCUS_BAND, 5], { entry: HeldItemId.FOCUS_BAND, weight: 1 },
[modifierTypes.KINGS_ROCK, 3], { entry: HeldItemId.KINGS_ROCK, weight: 1 },
[modifierTypes.GRIP_CLAW, 5], { entry: HeldItemId.GRIP_CLAW, weight: 1 },
]; ];
const berryPool = [
[BerryType.APICOT, 3],
[BerryType.ENIGMA, 2],
[BerryType.GANLON, 3],
[BerryType.LANSAT, 3],
[BerryType.LEPPA, 2],
[BerryType.LIECHI, 3],
[BerryType.LUM, 2],
[BerryType.PETAYA, 3],
[BerryType.SALAC, 2],
[BerryType.SITRUS, 2],
[BerryType.STARF, 3],
];
let pool: any[];
if (tier === "Berries") {
pool = berryPool;
} else {
pool = tier === ModifierTier.ULTRA ? ultraPool : roguePool;
}
for (let i = 0; i < numItems; i++) {
if (pool.length === 0) {
// Stop generating new items if somehow runs out of items to spawn
return;
}
const randIndex = randSeedInt(pool.length);
const newItemType = pool[randIndex];
let newMod: PokemonHeldItemModifierType;
if (tier === "Berries") {
newMod = generateModifierType(modifierTypes.BERRY, [newItemType[0]]) as PokemonHeldItemModifierType;
} else {
newMod = generateModifierType(newItemType[0]) as PokemonHeldItemModifierType;
}
applyModifierTypeToPlayerPokemon(pokemon, newMod);
// Decrement max stacks and remove from pool if at max
newItemType[1]--;
if (newItemType[1] <= 0) {
pool.splice(randIndex, 1);
}
}
}

View File

@ -16,10 +16,9 @@ import {
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { PokemonFormChangeItemModifier } from "#app/modifier/modifier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import type { HeldItemConfiguration } from "#app/items/held-item-data-types";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
const namespace = "mysteryEncounters/darkDeal"; const namespace = "mysteryEncounters/darkDeal";
@ -149,7 +148,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
const removedPokemon = getRandomPlayerPokemon(true, false, true); const removedPokemon = getRandomPlayerPokemon(true, false, true);
// Get all the pokemon's held items // Get all the pokemon's held items
const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); const itemConfig = removedPokemon.heldItemManager.generateHeldItemConfiguration();
globalScene.removePokemonFromPlayerParty(removedPokemon); globalScene.removePokemonFromPlayerParty(removedPokemon);
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
@ -158,7 +157,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
// Store removed pokemon types // Store removed pokemon types
encounter.misc = { encounter.misc = {
removedTypes: removedPokemon.getTypes(), removedTypes: removedPokemon.getTypes(),
modifiers, itemConfig: itemConfig,
}; };
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
@ -176,7 +175,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
bossTypes = singleTypeChallenges.map(c => (c.value - 1) as PokemonType); bossTypes = singleTypeChallenges.map(c => (c.value - 1) as PokemonType);
} }
const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers; const bossItemConfig: HeldItemConfiguration = encounter.misc.itemConfig;
// Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+
const roll = randSeedInt(100); const roll = randSeedInt(100);
const starterTier: number | [number, number] = roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [9, 10]; const starterTier: number | [number, number] = roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [9, 10];
@ -184,12 +183,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
const pokemonConfig: EnemyPokemonConfig = { const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies, species: bossSpecies,
isBoss: true, isBoss: true,
modifierConfigs: bossModifiers.map(m => { heldItemConfig: bossItemConfig,
return {
modifier: m,
stackCount: m.getStackCount(),
};
}),
}; };
if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) { if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) {
pokemonConfig.formIndex = 0; pokemonConfig.formIndex = 0;

View File

@ -9,25 +9,14 @@ import {
} from "#app/data/mystery-encounters/mystery-encounter-requirements"; } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { import {
generateModifierType,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
selectPokemonForOption, selectPokemonForOption,
updatePlayerMoney, updatePlayerMoney,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import type { PokemonHeldItemModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier";
import {
BerryModifier,
HealingBoosterModifier,
LevelIncrementBoosterModifier,
MoneyMultiplierModifier,
PreserveBerryModifier,
} from "#app/modifier/modifier";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
@ -37,31 +26,41 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { allHeldItems } from "#app/data/data-lists";
import { TrainerItemId } from "#enums/trainer-item-id";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/delibirdy"; const namespace = "mysteryEncounters/delibirdy";
/** Berries only */ /** Berries only */
const OPTION_2_ALLOWED_MODIFIERS = ["BerryModifier", "PokemonInstantReviveModifier"]; const OPTION_2_ALLOWED_MODIFIERS = [HeldItemCategoryId.BERRY, HeldItemId.REVIVER_SEED];
/** Disallowed items are berries, Reviver Seeds, and Vitamins (form change items and fusion items are not PokemonHeldItemModifiers) */ /** Disallowed items are berries, Reviver Seeds, and Vitamins (form change items and fusion items are not PokemonHeldItemModifiers) */
const OPTION_3_DISALLOWED_MODIFIERS = [ const OPTION_3_DISALLOWED_MODIFIERS = [HeldItemCategoryId.BERRY, HeldItemId.REVIVER_SEED];
"BerryModifier",
"PokemonInstantReviveModifier",
"TerastallizeModifier",
"PokemonBaseStatModifier",
"PokemonBaseStatTotalModifier",
];
const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2; const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2;
async function backupOption() {
globalScene.getPlayerPokemon()?.heldItemManager.add(HeldItemId.SHELL_BELL);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", {
modifierName: allHeldItems[HeldItemId.SHELL_BELL].name,
}),
null,
undefined,
true,
);
doEventReward();
}
const doEventReward = () => { const doEventReward = () => {
const event_buff = timedEventManager.getDelibirdyBuff(); const event_buff = timedEventManager.getDelibirdyBuff();
if (event_buff.length > 0) { if (event_buff.length > 0) {
const candidates = event_buff.filter(c => { const candidates = event_buff.filter(c => {
const mtype = generateModifierType(modifierTypes[c]); const fullStack = globalScene.trainerItems.isMaxStack(c);
const existingCharm = globalScene.findModifier(m => m.type.id === mtype?.id); return !fullStack;
return !(existingCharm && existingCharm.getStackCount() >= existingCharm.getMaxStackCount());
}); });
if (candidates.length > 0) { if (candidates.length > 0) {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes[randSeedItem(candidates)]); globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes[randSeedItem(candidates)]);
@ -165,20 +164,11 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
.withOptionPhase(async () => { .withOptionPhase(async () => {
// Give the player an Amulet Coin // Give the player an Amulet Coin
// Check if the player has max stacks of that item already // Check if the player has max stacks of that item already
const existing = globalScene.findModifier(m => m instanceof MoneyMultiplierModifier) as MoneyMultiplierModifier; const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.AMULET_COIN);
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { if (fullStack) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; backupOption();
await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", { modifierName: shellBell.name }),
null,
undefined,
true,
);
doEventReward();
} else { } else {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.AMULET_COIN); globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.AMULET_COIN);
doEventReward(); doEventReward();
@ -205,19 +195,17 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(it => { const validItems = pokemon.heldItemManager.filterRequestedItems(OPTION_2_ALLOWED_MODIFIERS, true);
return OPTION_2_ALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable;
});
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((item: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: modifier.type.name, label: allHeldItems[item].name,
handler: () => { handler: () => {
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.setDialogueToken("chosenItem", allHeldItems[item].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
chosenModifier: modifier, chosenItem: item,
}; };
return true; return true;
}, },
@ -240,59 +228,35 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const modifier: BerryModifier | PokemonInstantReviveModifier = encounter.misc.chosenModifier; const chosenItem: HeldItemId = encounter.misc.chosenItem;
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed
if (modifier instanceof BerryModifier) { if (isItemInCategory(chosenItem, HeldItemCategoryId.BERRY)) {
// Check if the player has max stacks of that Candy Jar already // Check if the player has max stacks of that Candy Jar already
const existing = globalScene.findModifier( const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.CANDY_JAR);
m => m instanceof LevelIncrementBoosterModifier,
) as LevelIncrementBoosterModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { if (fullStack) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; backupOption();
await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", {
modifierName: shellBell.name,
}),
null,
undefined,
true,
);
doEventReward();
} else { } else {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.CANDY_JAR); globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.CANDY_JAR);
doEventReward(); doEventReward();
} }
} else { } else {
// Check if the player has max stacks of that Berry Pouch already // Check if the player has max stacks of that Berry Pouch already
const existing = globalScene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.BERRY_POUCH);
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { if (fullStack) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; backupOption();
await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", {
modifierName: shellBell.name,
}),
null,
undefined,
true,
);
doEventReward();
} else { } else {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.BERRY_POUCH); globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.BERRY_POUCH);
doEventReward(); doEventReward();
} }
} }
chosenPokemon.loseHeldItem(modifier, false); chosenPokemon.loseHeldItem(chosenItem, false);
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
}) })
@ -315,21 +279,17 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(it => { const validItems = pokemon.heldItemManager.filterRequestedItems(OPTION_3_DISALLOWED_MODIFIERS, true, true);
return (
!OPTION_3_DISALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable
);
});
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((item: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: modifier.type.name, label: allHeldItems[item].name,
handler: () => { handler: () => {
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.setDialogueToken("chosenItem", allHeldItems[item].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
chosenModifier: modifier, chosenItem: item,
}; };
return true; return true;
}, },
@ -356,20 +316,12 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Check if the player has max stacks of Healing Charm already // Check if the player has max stacks of Healing Charm already
const existing = globalScene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.HEALING_CHARM);
if (fullStack) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; backupOption();
await applyModifierTypeToPlayerPokemon(globalScene.getPlayerParty()[0], shellBell);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", { modifierName: shellBell.name }),
null,
undefined,
true,
);
doEventReward();
} else { } else {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.HEALING_CHARM); globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.HEALING_CHARM);
doEventReward(); doEventReward();

View File

@ -7,9 +7,7 @@ import {
setEncounterExp, setEncounterExp,
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
generateModifierType,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
@ -35,7 +33,6 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun
import { import {
applyAbilityOverrideToPokemon, applyAbilityOverrideToPokemon,
applyDamageToPokemon, applyDamageToPokemon,
applyModifierTypeToPlayerPokemon,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -45,8 +42,11 @@ import { AbilityId } from "#enums/ability-id";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { getNewHeldItemFromCategory } from "#app/items/held-item-pool";
import { allHeldItems } from "#app/data/data-lists";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { allAbilities, modifierTypes } from "#app/data/data-lists"; import { allAbilities } from "#app/data/data-lists";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fieryFallout"; const namespace = "mysteryEncounters/fieryFallout";
@ -302,16 +302,14 @@ function giveLeadPokemonAttackTypeBoostItem() {
const leadPokemon = globalScene.getPlayerParty()?.[0]; const leadPokemon = globalScene.getPlayerParty()?.[0];
if (leadPokemon) { if (leadPokemon) {
// Generate type booster held item, default to Charcoal if item fails to generate // Generate type booster held item, default to Charcoal if item fails to generate
let boosterModifierType = generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType; let item = getNewHeldItemFromCategory(HeldItemCategoryId.TYPE_ATTACK_BOOSTER, leadPokemon);
if (!boosterModifierType) { if (!item) {
boosterModifierType = generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [ item = HeldItemId.CHARCOAL;
PokemonType.FIRE,
]) as AttackTypeBoosterModifierType;
} }
applyModifierTypeToPlayerPokemon(leadPokemon, boosterModifierType); leadPokemon.heldItemManager.add(item);
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
encounter.setDialogueToken("itemName", boosterModifierType.name); encounter.setDialogueToken("itemName", allHeldItems[item].name);
encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender());
queueEncounterMessage(`${namespace}:found_item`); queueEncounterMessage(`${namespace}:found_item`);
} }

View File

@ -9,7 +9,7 @@ import {
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import type { ModifierTypeOption } from "#app/modifier/modifier-type";
import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
@ -89,12 +89,12 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder.
// Waves 10-40 GREAT, 60-120 ULTRA, 120-160 ROGUE, 160-180 MASTER // Waves 10-40 GREAT, 60-120 ULTRA, 120-160 ROGUE, 160-180 MASTER
const tier = const tier =
globalScene.currentBattle.waveIndex > 160 globalScene.currentBattle.waveIndex > 160
? ModifierTier.MASTER ? RewardTier.MASTER
: globalScene.currentBattle.waveIndex > 120 : globalScene.currentBattle.waveIndex > 120
? ModifierTier.ROGUE ? RewardTier.ROGUE
: globalScene.currentBattle.waveIndex > 40 : globalScene.currentBattle.waveIndex > 40
? ModifierTier.ULTRA ? RewardTier.ULTRA
: ModifierTier.GREAT; : RewardTier.GREAT;
regenerateModifierPoolThresholds(globalScene.getPlayerParty(), ModifierPoolType.PLAYER, 0); regenerateModifierPoolThresholds(globalScene.getPlayerParty(), ModifierPoolType.PLAYER, 0);
let item: ModifierTypeOption | null = null; let item: ModifierTypeOption | null = null;
// TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward // TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward

View File

@ -388,7 +388,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise<void> {
globalScene.add.existing(pokemon); globalScene.add.existing(pokemon);
globalScene.field.add(pokemon); globalScene.field.add(pokemon);
addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.pokeball); addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.pokeball);
globalScene.updateModifiers(true); globalScene.updateItems(true);
globalScene.updateFieldScale(); globalScene.updateFieldScale();
pokemon.showInfo(); pokemon.showInfo();
pokemon.playAnim(); pokemon.playAnim();

View File

@ -4,7 +4,6 @@ import {
setEncounterRewards, setEncounterRewards,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import { ModifierTier } from "#enums/modifier-tier";
import { MusicPreference } from "#app/system/settings/settings"; import { MusicPreference } from "#app/system/settings/settings";
import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import type { ModifierTypeOption } from "#app/modifier/modifier-type";
import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
@ -33,13 +32,6 @@ import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { EnemyPokemon } from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
import {
HiddenAbilityRateBoosterModifier,
PokemonFormChangeItemModifier,
ShinyRateBoosterModifier,
SpeciesStatBoosterModifier,
} from "#app/modifier/modifier";
import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import PokemonData from "#app/system/pokemon-data"; import PokemonData from "#app/system/pokemon-data";
import i18next from "i18next"; import i18next from "i18next";
@ -53,6 +45,11 @@ import type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims"; import { doShinySparkleAnim } from "#app/field/anims";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { HeldItemCategoryId, type HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { allHeldItems } from "#app/data/data-lists";
import { RewardTier } from "#enums/reward-tier";
import { getHeldItemTier } from "#app/items/held-item-tiers";
import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/globalTradeSystem"; const namespace = "mysteryEncounters/globalTradeSystem";
@ -215,9 +212,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon;
const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon;
const modifiers = tradedPokemon const heldItemConfig = tradedPokemon.heldItemManager
.getHeldItems() .generateHeldItemConfiguration()
.filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); .filter(ic => !isItemInCategory(ic.entry as HeldItemId, HeldItemCategoryId.SPECIES_STAT_BOOSTER));
// Generate a trainer name // Generate a trainer name
const traderName = generateRandomTraderName(); const traderName = generateRandomTraderName();
@ -241,16 +238,12 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
dataSource.variant, dataSource.variant,
dataSource.ivs, dataSource.ivs,
dataSource.nature, dataSource.nature,
heldItemConfig,
dataSource, dataSource,
); );
globalScene.getPlayerParty().push(newPlayerPokemon); globalScene.getPlayerParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets(); await newPlayerPokemon.loadAssets();
for (const mod of modifiers) {
mod.pokemonId = newPlayerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
// Show the trade animation // Show the trade animation
await showTradeBackground(); await showTradeBackground();
await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon); await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon);
@ -283,7 +276,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
if (timedEventManager.isEventActive()) { if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier(); shinyThreshold.value *= timedEventManager.getShinyMultiplier();
} }
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold });
// Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms // Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms
// Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that // Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that
@ -297,7 +290,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
if (tradePokemon.species.abilityHidden) { if (tradePokemon.species.abilityHidden) {
if (tradePokemon.abilityIndex < hiddenIndex) { if (tradePokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new NumberHolder(64); const hiddenAbilityChance = new NumberHolder(64);
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
@ -336,9 +331,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon;
const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon;
const modifiers = tradedPokemon const heldItemConfig = tradedPokemon.heldItemManager
.getHeldItems() .generateHeldItemConfiguration()
.filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); .filter(ic => !isItemInCategory(ic.entry as HeldItemId, HeldItemCategoryId.SPECIES_STAT_BOOSTER));
// Generate a trainer name // Generate a trainer name
const traderName = generateRandomTraderName(); const traderName = generateRandomTraderName();
@ -361,16 +356,12 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
dataSource.variant, dataSource.variant,
dataSource.ivs, dataSource.ivs,
dataSource.nature, dataSource.nature,
heldItemConfig,
dataSource, dataSource,
); );
globalScene.getPlayerParty().push(newPlayerPokemon); globalScene.getPlayerParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets(); await newPlayerPokemon.loadAssets();
for (const mod of modifiers) {
mod.pokemonId = newPlayerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
// Show the trade animation // Show the trade animation
await showTradeBackground(); await showTradeBackground();
await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon); await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon);
@ -395,17 +386,15 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(it => { const validItems = pokemon.heldItemManager.getTransferableHeldItems();
return it.isTransferable;
});
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((id: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: modifier.type.name, label: allHeldItems[id].name,
handler: () => { handler: () => {
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.setDialogueToken("chosenItem", allHeldItems[id].name);
encounter.misc.chosenModifier = modifier; encounter.misc.chosenHeldItem = id;
encounter.misc.chosenPokemon = pokemon; encounter.misc.chosenPokemon = pokemon;
return true; return true;
}, },
@ -416,10 +405,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon has items to trade // If pokemon has items to trade
const meetsReqs = const meetsReqs = pokemon.heldItemManager.getTransferableHeldItems().length > 0;
pokemon.getHeldItems().filter(it => {
return it.isTransferable;
}).length > 0;
if (!meetsReqs) { if (!meetsReqs) {
return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null;
} }
@ -431,23 +417,15 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier; const heldItemId = encounter.misc.chosenHeldItem as HeldItemId;
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Check tier of the traded item, the received item will be one tier up // Check tier of the traded item, the received item will be one tier up
const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party); let tier = getHeldItemTier(heldItemId) ?? RewardTier.GREAT;
let tier = type.tier ?? ModifierTier.GREAT;
// Eggs and White Herb are not in the pool
if (type.id === "WHITE_HERB") {
tier = ModifierTier.GREAT;
} else if (type.id === "LUCKY_EGG") {
tier = ModifierTier.ULTRA;
} else if (type.id === "GOLDEN_EGG") {
tier = ModifierTier.ROGUE;
}
// Increment tier by 1 // Increment tier by 1
if (tier < ModifierTier.MASTER) { if (tier < RewardTier.MASTER) {
tier++; tier++;
} }
@ -467,8 +445,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
fillRemaining: false, fillRemaining: false,
}); });
chosenPokemon.loseHeldItem(modifier, false); chosenPokemon.heldItemManager.remove(heldItemId);
await globalScene.updateModifiers(true, true); await globalScene.updateItems(true);
// Generate a trainer name // Generate a trainer name
const traderName = generateRandomTraderName(); const traderName = generateRandomTraderName();

View File

@ -7,7 +7,7 @@ import { trainerConfigs } from "#app/data/trainers/trainer-config";
import { trainerPartyTemplates } from "#app/data/trainers/TrainerPartyTemplate"; import { trainerPartyTemplates } from "#app/data/trainers/TrainerPartyTemplate";
import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTemplate";
import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
@ -176,7 +176,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1];
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.GREAT, RewardTier.GREAT],
fillRemaining: true, fillRemaining: true,
}); });
@ -207,7 +207,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter
encounter.expMultiplier = 0.9; encounter.expMultiplier = 0.9;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], guaranteedModifierTiers: [RewardTier.ROGUE, RewardTier.ROGUE, RewardTier.ULTRA, RewardTier.GREAT],
fillRemaining: true, fillRemaining: true,
}); });

View File

@ -16,7 +16,7 @@ import {
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -144,7 +144,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde
if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT) { if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT) {
// Choose between 2 COMMON / 2 GREAT tier items (20%) // Choose between 2 COMMON / 2 GREAT tier items (20%)
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.GREAT], guaranteedModifierTiers: [RewardTier.COMMON, RewardTier.COMMON, RewardTier.GREAT, RewardTier.GREAT],
}); });
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(`${namespace}:option.1.normal`); queueEncounterMessage(`${namespace}:option.1.normal`);
@ -152,7 +152,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde
} else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT) { } else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT) {
// Choose between 3 ULTRA tier items (30%) // Choose between 3 ULTRA tier items (30%)
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], guaranteedModifierTiers: [RewardTier.ULTRA, RewardTier.ULTRA, RewardTier.ULTRA],
}); });
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(`${namespace}:option.1.good`); queueEncounterMessage(`${namespace}:option.1.good`);
@ -160,7 +160,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde
} else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT - ROGUE_REWARDS_PERCENT) { } else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT - ROGUE_REWARDS_PERCENT) {
// Choose between 2 ROGUE tier items (10%) // Choose between 2 ROGUE tier items (10%)
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], guaranteedModifierTiers: [RewardTier.ROGUE, RewardTier.ROGUE],
}); });
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(`${namespace}:option.1.great`); queueEncounterMessage(`${namespace}:option.1.great`);
@ -171,7 +171,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde
) { ) {
// Choose 1 MASTER tier item (5%) // Choose 1 MASTER tier item (5%)
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.MASTER], guaranteedModifierTiers: [RewardTier.MASTER],
}); });
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(`${namespace}:option.1.amazing`); queueEncounterMessage(`${namespace}:option.1.amazing`);

View File

@ -11,7 +11,6 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en
import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier";
import type { EnemyPokemon } from "#app/field/pokemon"; import type { EnemyPokemon } from "#app/field/pokemon";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
@ -31,6 +30,8 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups"; import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
import { TrainerItemId } from "#enums/trainer-item-id";
import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/safariZone"; const namespace = "mysteryEncounters/safariZone";
@ -297,7 +298,9 @@ async function summonSafariPokemon() {
const hiddenIndex = pokemon.species.ability2 ? 2 : 1; const hiddenIndex = pokemon.species.ability2 ? 2 : 1;
if (pokemon.abilityIndex < hiddenIndex) { if (pokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new NumberHolder(256); const hiddenAbilityChance = new NumberHolder(256);
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
@ -332,8 +335,7 @@ async function summonSafariPokemon() {
// shows up and the IV scanner breaks. For now, we place the IV scanner code // shows up and the IV scanner breaks. For now, we place the IV scanner code
// separately so that at least the IV scanner works. // separately so that at least the IV scanner works.
const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); if (globalScene.trainerItems.hasItem(TrainerItemId.IV_SCANNER)) {
if (ivScannerModifier) {
globalScene.phaseManager.pushNew("ScanIvsPhase", pokemon.getBattlerIndex()); globalScene.phaseManager.pushNew("ScanIvsPhase", pokemon.getBattlerIndex());
} }
} }

View File

@ -1,5 +1,4 @@
import { import {
generateModifierType,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
selectPokemonForOption, selectPokemonForOption,
setEncounterExp, setEncounterExp,
@ -7,7 +6,6 @@ import {
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { modifierTypes } from "#app/data/data-lists";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
@ -19,7 +17,6 @@ import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { import {
applyDamageToPokemon, applyDamageToPokemon,
applyModifierTypeToPlayerPokemon,
isPokemonValidForEncounterOptionSelection, isPokemonValidForEncounterOptionSelection,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -28,6 +25,8 @@ import type { Nature } from "#enums/nature";
import { getNatureName } from "#app/data/nature"; import { getNatureName } from "#app/data/nature";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import i18next from "i18next"; import i18next from "i18next";
import { getNewVitaminHeldItem } from "#app/items/held-item-pool";
import { allHeldItems } from "#app/data/data-lists";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/shadyVitaminDealer"; const namespace = "mysteryEncounters/shadyVitaminDealer";
@ -97,15 +96,12 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
// Update money // Update money
updatePlayerMoney(-(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); updatePlayerMoney(-(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney);
// Calculate modifiers and dialogue tokens // Calculate modifiers and dialogue tokens
const modifiers = [ const items = [getNewVitaminHeldItem(), getNewVitaminHeldItem()];
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, encounter.setDialogueToken("boost1", allHeldItems[items[0]].name);
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, encounter.setDialogueToken("boost2", allHeldItems[items[1]].name);
];
encounter.setDialogueToken("boost1", modifiers[0].name);
encounter.setDialogueToken("boost2", modifiers[1].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
modifiers: modifiers, items: items,
}; };
}; };
@ -132,10 +128,10 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
// Choose Cheap Option // Choose Cheap Option
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const chosenPokemon = encounter.misc.chosenPokemon; const chosenPokemon = encounter.misc.chosenPokemon;
const modifiers = encounter.misc.modifiers; const items = encounter.misc.items;
for (const modType of modifiers) { for (const item of items) {
await applyModifierTypeToPlayerPokemon(chosenPokemon, modType); chosenPokemon.heldItemManager.add(item);
} }
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
@ -180,15 +176,12 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
// Update money // Update money
updatePlayerMoney(-(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney); updatePlayerMoney(-(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney);
// Calculate modifiers and dialogue tokens // Calculate modifiers and dialogue tokens
const modifiers = [ const items = [getNewVitaminHeldItem(), getNewVitaminHeldItem()];
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, encounter.setDialogueToken("boost1", allHeldItems[items[0]].name);
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, encounter.setDialogueToken("boost2", allHeldItems[items[1]].name);
];
encounter.setDialogueToken("boost1", modifiers[0].name);
encounter.setDialogueToken("boost2", modifiers[1].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
modifiers: modifiers, items: items,
}; };
}; };
@ -203,10 +196,10 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
// Choose Expensive Option // Choose Expensive Option
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const chosenPokemon = encounter.misc.chosenPokemon; const chosenPokemon = encounter.misc.chosenPokemon;
const modifiers = encounter.misc.modifiers; const items = encounter.misc.items;
for (const modType of modifiers) { for (const item of items) {
await applyModifierTypeToPlayerPokemon(chosenPokemon, modType); chosenPokemon.heldItemManager.add(item);
} }
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);

View File

@ -1,5 +1,4 @@
import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
@ -11,7 +10,6 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst
import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import type { EnemyPartyConfig, EnemyPokemonConfig } from "../utils/encounter-phase-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "../utils/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
loadCustomMovesForEncounter, loadCustomMovesForEncounter,
@ -27,10 +25,9 @@ import { AiType } from "#enums/ai-type";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat";
import { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { HeldItemId } from "#enums/held-item-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
/** i18n namespace for the encounter */ /** i18n namespace for the encounter */
@ -78,24 +75,12 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
status: [StatusEffect.SLEEP, 6], // Extra turns on timer for Snorlax's start of fight moves status: [StatusEffect.SLEEP, 6], // Extra turns on timer for Snorlax's start of fight moves
nature: Nature.DOCILE, nature: Nature.DOCILE,
moveSet: [MoveId.BODY_SLAM, MoveId.CRUNCH, MoveId.SLEEP_TALK, MoveId.REST], moveSet: [MoveId.BODY_SLAM, MoveId.CRUNCH, MoveId.SLEEP_TALK, MoveId.REST],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.SITRUS_BERRY, count: 1 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, { entry: HeldItemId.ENIGMA_BERRY, count: 1 },
}, { entry: HeldItemId.HP_UP, count: 1 },
{ { entry: HeldItemId.SOOTHE_BELL, count: randSeedInt(2, 0) },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, { entry: HeldItemId.LUCKY_EGG, count: randSeedInt(2, 0) },
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
{
modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
], ],
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
aiType: AiType.SMART, // Required to ensure Snorlax uses Sleep Talk while it is asleep aiType: AiType.SMART, // Required to ensure Snorlax uses Sleep Talk while it is asleep

View File

@ -614,7 +614,6 @@ function removePokemonFromPartyAndStoreHeldItems(encounter: MysteryEncounter, ch
party[chosenIndex] = party[0]; party[chosenIndex] = party[0];
party[0] = chosenPokemon; party[0] = chosenPokemon;
encounter.misc.originalParty = globalScene.getPlayerParty().slice(1); encounter.misc.originalParty = globalScene.getPlayerParty().slice(1);
encounter.misc.originalPartyHeldItems = encounter.misc.originalParty.map(p => p.getHeldItems());
globalScene["party"] = [chosenPokemon]; globalScene["party"] = [chosenPokemon];
} }
@ -623,14 +622,7 @@ function restorePartyAndHeldItems() {
// Restore original party // Restore original party
globalScene.getPlayerParty().push(...encounter.misc.originalParty); globalScene.getPlayerParty().push(...encounter.misc.originalParty);
// Restore held items globalScene.updateItems(true);
const originalHeldItems = encounter.misc.originalPartyHeldItems;
for (const pokemonHeldItemsList of originalHeldItems) {
for (const heldItem of pokemonHeldItemsList) {
globalScene.addModifier(heldItem, true, false, false, true);
}
}
globalScene.updateModifiers(true);
} }
function onGameOver() { function onGameOver() {

View File

@ -5,9 +5,7 @@ import {
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
generateModifierType,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
@ -19,15 +17,14 @@ import { Nature } from "#enums/nature";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { HeldItemId } from "#enums/held-item-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
@ -95,23 +92,12 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.HARDY, nature: Nature.HARDY,
moveSet: [MoveId.INFESTATION, MoveId.SALT_CURE, MoveId.GASTRO_ACID, MoveId.HEAL_ORDER], moveSet: [MoveId.INFESTATION, MoveId.SALT_CURE, MoveId.GASTRO_ACID, MoveId.HEAL_ORDER],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.SITRUS_BERRY, count: 1 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, { entry: HeldItemId.ENIGMA_BERRY, count: 1 },
}, { entry: HeldItemId.APICOT_BERRY, count: 1 },
{ { entry: HeldItemId.GANLON_BERRY, count: 1 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, { entry: HeldItemId.LUM_BERRY, count: 2 },
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType,
stackCount: 2,
},
], ],
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => { mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
@ -171,11 +157,11 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
sortedParty.forEach((pokemon, index) => { sortedParty.forEach((pokemon, index) => {
if (index < 2) { if (index < 2) {
// -15 to the two highest BST mons // -15 to the two highest BST mons
modifyPlayerPokemonBST(pokemon, false); pokemon.heldItemManager.add(HeldItemId.SHUCKLE_JUICE_BAD);
encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender()); encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender());
} else { } else {
// +10 for the rest // +10 for the rest
modifyPlayerPokemonBST(pokemon, true); pokemon.heldItemManager.add(HeldItemId.SHUCKLE_JUICE_GOOD);
} }
}); });

View File

@ -1,14 +1,11 @@
import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
generateModifierType,
generateModifierTypeOption, generateModifierTypeOption,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
@ -20,17 +17,18 @@ import { AbilityId } from "#enums/ability-id";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { PokemonType } from "#enums/pokemon-type";
import { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat";
import { SpeciesFormChangeAbilityTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { SpeciesFormChangeAbilityTrigger } from "#app/data/pokemon-forms/form-change-triggers";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import i18next from "i18next"; import i18next from "i18next";
import { ModifierTier } from "#enums/modifier-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { modifierTypes } from "#app/data/data-lists";
import { RewardTier } from "#enums/reward-tier";
import { HeldItemId } from "#enums/held-item-id";
//TODO: make all items unstealable
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theWinstrateChallenge"; const namespace = "mysteryEncounters/theWinstrateChallenge";
@ -166,7 +164,7 @@ async function spawnNextTrainerOrEndEncounter() {
await showEncounterDialogue(`${namespace}:victory_2`, `${namespace}:speaker`); await showEncounterDialogue(`${namespace}:victory_2`, `${namespace}:speaker`);
globalScene.ui.clearText(); // Clears "Winstrate" title from screen as rewards get animated in globalScene.ui.clearText(); // Clears "Winstrate" title from screen as rewards get animated in
const machoBrace = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_MACHO_BRACE)!; const machoBrace = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_MACHO_BRACE)!;
machoBrace.type.tier = ModifierTier.MASTER; machoBrace.type.tier = RewardTier.MASTER;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: [machoBrace], guaranteedModifierTypeOptions: [machoBrace],
fillRemaining: false, fillRemaining: false,
@ -258,16 +256,9 @@ function getVictorTrainerConfig(): EnemyPartyConfig {
abilityIndex: 0, // Guts abilityIndex: 0, // Guts
nature: Nature.ADAMANT, nature: Nature.ADAMANT,
moveSet: [MoveId.FACADE, MoveId.BRAVE_BIRD, MoveId.PROTECT, MoveId.QUICK_ATTACK], moveSet: [MoveId.FACADE, MoveId.BRAVE_BIRD, MoveId.PROTECT, MoveId.QUICK_ATTACK],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.FLAME_ORB, count: 1 },
modifier: generateModifierType(modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, { entry: HeldItemId.FOCUS_BAND, count: 2 },
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.FOCUS_BAND) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
], ],
}, },
{ {
@ -276,16 +267,9 @@ function getVictorTrainerConfig(): EnemyPartyConfig {
abilityIndex: 1, // Guts abilityIndex: 1, // Guts
nature: Nature.ADAMANT, nature: Nature.ADAMANT,
moveSet: [MoveId.FACADE, MoveId.OBSTRUCT, MoveId.NIGHT_SLASH, MoveId.FIRE_PUNCH], moveSet: [MoveId.FACADE, MoveId.OBSTRUCT, MoveId.NIGHT_SLASH, MoveId.FIRE_PUNCH],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.FLAME_ORB, count: 1 },
modifier: generateModifierType(modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, { entry: HeldItemId.LEFTOVERS, count: 2 },
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
], ],
}, },
], ],
@ -302,16 +286,9 @@ function getVictoriaTrainerConfig(): EnemyPartyConfig {
abilityIndex: 0, // Natural Cure abilityIndex: 0, // Natural Cure
nature: Nature.CALM, nature: Nature.CALM,
moveSet: [MoveId.SYNTHESIS, MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.SLEEP_POWDER], moveSet: [MoveId.SYNTHESIS, MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.SLEEP_POWDER],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.SOUL_DEW, count: 1 },
modifier: generateModifierType(modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType, { entry: HeldItemId.QUICK_CLAW, count: 2 },
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
], ],
}, },
{ {
@ -320,21 +297,9 @@ function getVictoriaTrainerConfig(): EnemyPartyConfig {
formIndex: 1, formIndex: 1,
nature: Nature.TIMID, nature: Nature.TIMID,
moveSet: [MoveId.PSYSHOCK, MoveId.MOONBLAST, MoveId.SHADOW_BALL, MoveId.WILL_O_WISP], moveSet: [MoveId.PSYSHOCK, MoveId.MOONBLAST, MoveId.SHADOW_BALL, MoveId.WILL_O_WISP],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.TWISTED_SPOON, count: 1 },
modifier: generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [ { entry: HeldItemId.FAIRY_FEATHER, count: 1 },
PokemonType.PSYCHIC,
]) as PokemonHeldItemModifierType,
stackCount: 1,
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [
PokemonType.FAIRY,
]) as PokemonHeldItemModifierType,
stackCount: 1,
isTransferable: false,
},
], ],
}, },
], ],
@ -351,17 +316,9 @@ function getViviTrainerConfig(): EnemyPartyConfig {
abilityIndex: 3, // Lightning Rod abilityIndex: 3, // Lightning Rod
nature: Nature.ADAMANT, nature: Nature.ADAMANT,
moveSet: [MoveId.WATERFALL, MoveId.MEGAHORN, MoveId.KNOCK_OFF, MoveId.REST], moveSet: [MoveId.WATERFALL, MoveId.MEGAHORN, MoveId.KNOCK_OFF, MoveId.REST],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.LUM_BERRY, count: 2 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, { entry: HeldItemId.HP_UP, count: 4 },
stackCount: 2,
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType,
stackCount: 4,
isTransferable: false,
},
], ],
}, },
{ {
@ -370,16 +327,9 @@ function getViviTrainerConfig(): EnemyPartyConfig {
abilityIndex: 1, // Poison Heal abilityIndex: 1, // Poison Heal
nature: Nature.JOLLY, nature: Nature.JOLLY,
moveSet: [MoveId.SPORE, MoveId.SWORDS_DANCE, MoveId.SEED_BOMB, MoveId.DRAIN_PUNCH], moveSet: [MoveId.SPORE, MoveId.SWORDS_DANCE, MoveId.SEED_BOMB, MoveId.DRAIN_PUNCH],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.HP_UP, count: 4 },
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, { entry: HeldItemId.TOXIC_ORB, count: 1 },
stackCount: 4,
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType,
isTransferable: false,
},
], ],
}, },
{ {
@ -388,13 +338,7 @@ function getViviTrainerConfig(): EnemyPartyConfig {
formIndex: 1, formIndex: 1,
nature: Nature.CALM, nature: Nature.CALM,
moveSet: [MoveId.EARTH_POWER, MoveId.FIRE_BLAST, MoveId.YAWN, MoveId.PROTECT], moveSet: [MoveId.EARTH_POWER, MoveId.FIRE_BLAST, MoveId.YAWN, MoveId.PROTECT],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.QUICK_CLAW, count: 3 }],
{
modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType,
stackCount: 3,
isTransferable: false,
},
],
}, },
], ],
}; };
@ -410,12 +354,7 @@ function getVickyTrainerConfig(): EnemyPartyConfig {
formIndex: 1, formIndex: 1,
nature: Nature.IMPISH, nature: Nature.IMPISH,
moveSet: [MoveId.AXE_KICK, MoveId.ICE_PUNCH, MoveId.ZEN_HEADBUTT, MoveId.BULLET_PUNCH], moveSet: [MoveId.AXE_KICK, MoveId.ICE_PUNCH, MoveId.ZEN_HEADBUTT, MoveId.BULLET_PUNCH],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.SHELL_BELL, count: 1 }],
{
modifier: generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType,
isTransferable: false,
},
],
}, },
], ],
}; };
@ -431,13 +370,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
abilityIndex: 0, // Soundproof abilityIndex: 0, // Soundproof
nature: Nature.MODEST, nature: Nature.MODEST,
moveSet: [MoveId.THUNDERBOLT, MoveId.GIGA_DRAIN, MoveId.FOUL_PLAY, MoveId.THUNDER_WAVE], moveSet: [MoveId.THUNDERBOLT, MoveId.GIGA_DRAIN, MoveId.FOUL_PLAY, MoveId.THUNDER_WAVE],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.ZINC, count: 2 }],
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.SPD]) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
],
}, },
{ {
species: getPokemonSpecies(SpeciesId.SWALOT), species: getPokemonSpecies(SpeciesId.SWALOT),
@ -445,51 +378,18 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
abilityIndex: 2, // Gluttony abilityIndex: 2, // Gluttony
nature: Nature.QUIET, nature: Nature.QUIET,
moveSet: [MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.ICE_BEAM, MoveId.EARTHQUAKE], moveSet: [MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.ICE_BEAM, MoveId.EARTHQUAKE],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.SITRUS_BERRY, count: 2 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, { entry: HeldItemId.APICOT_BERRY, count: 2 },
stackCount: 2, { entry: HeldItemId.GANLON_BERRY, count: 2 },
}, { entry: HeldItemId.STARF_BERRY, count: 2 },
{ { entry: HeldItemId.SALAC_BERRY, count: 2 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType, { entry: HeldItemId.LUM_BERRY, count: 2 },
stackCount: 2, { entry: HeldItemId.LANSAT_BERRY, count: 2 },
}, { entry: HeldItemId.LIECHI_BERRY, count: 2 },
{ { entry: HeldItemId.PETAYA_BERRY, count: 2 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType, { entry: HeldItemId.ENIGMA_BERRY, count: 2 },
stackCount: 2, { entry: HeldItemId.LEPPA_BERRY, count: 2 },
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.STARF]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SALAC]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LANSAT]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LIECHI]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.PETAYA]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LEPPA]) as PokemonHeldItemModifierType,
stackCount: 2,
},
], ],
}, },
{ {
@ -498,13 +398,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
abilityIndex: 2, // Tangled Feet abilityIndex: 2, // Tangled Feet
nature: Nature.JOLLY, nature: Nature.JOLLY,
moveSet: [MoveId.DRILL_PECK, MoveId.QUICK_ATTACK, MoveId.THRASH, MoveId.KNOCK_OFF], moveSet: [MoveId.DRILL_PECK, MoveId.QUICK_ATTACK, MoveId.THRASH, MoveId.KNOCK_OFF],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.KINGS_ROCK, count: 2 }],
{
modifier: generateModifierType(modifierTypes.KINGS_ROCK) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
],
}, },
{ {
species: getPokemonSpecies(SpeciesId.ALAKAZAM), species: getPokemonSpecies(SpeciesId.ALAKAZAM),
@ -512,13 +406,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
formIndex: 1, formIndex: 1,
nature: Nature.BOLD, nature: Nature.BOLD,
moveSet: [MoveId.PSYCHIC, MoveId.SHADOW_BALL, MoveId.FOCUS_BLAST, MoveId.THUNDERBOLT], moveSet: [MoveId.PSYCHIC, MoveId.SHADOW_BALL, MoveId.FOCUS_BLAST, MoveId.THUNDERBOLT],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.WIDE_LENS, count: 2 }],
{
modifier: generateModifierType(modifierTypes.WIDE_LENS) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
],
}, },
{ {
species: getPokemonSpecies(SpeciesId.DARMANITAN), species: getPokemonSpecies(SpeciesId.DARMANITAN),
@ -526,13 +414,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
abilityIndex: 0, // Sheer Force abilityIndex: 0, // Sheer Force
nature: Nature.IMPISH, nature: Nature.IMPISH,
moveSet: [MoveId.EARTHQUAKE, MoveId.U_TURN, MoveId.FLARE_BLITZ, MoveId.ROCK_SLIDE], moveSet: [MoveId.EARTHQUAKE, MoveId.U_TURN, MoveId.FLARE_BLITZ, MoveId.ROCK_SLIDE],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.QUICK_CLAW, count: 2 }],
{
modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
],
}, },
], ],
}; };

View File

@ -11,7 +11,6 @@ import { getNatureName } from "#app/data/nature";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { AbilityAttr } from "#enums/ability-attr"; import { AbilityAttr } from "#enums/ability-attr";
import PokemonData from "#app/system/pokemon-data"; import PokemonData from "#app/system/pokemon-data";
import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
@ -25,7 +24,6 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import type HeldModifierConfig from "#app/@types/held-modifier-config";
import i18next from "i18next"; import i18next from "i18next";
import { getStatKey } from "#enums/stat"; import { getStatKey } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
@ -102,8 +100,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
// Spawn light training session with chosen pokemon // Spawn light training session with chosen pokemon
// Every 50 waves, add +1 boss segment, capping at 5 // Every 50 waves, add +1 boss segment, capping at 5
const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 50), 5); const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 50), 5);
const modifiers = new ModifiersHolder(); const config = getEnemyConfig(playerPokemon, segments);
const config = getEnemyConfig(playerPokemon, segments, modifiers);
globalScene.removePokemonFromPlayerParty(playerPokemon, false); globalScene.removePokemonFromPlayerParty(playerPokemon, false);
const onBeforeRewardsPhase = () => { const onBeforeRewardsPhase = () => {
@ -152,13 +149,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
globalScene.gameData.setPokemonCaught(playerPokemon, false); globalScene.gameData.setPokemonCaught(playerPokemon, false);
} }
// Add pokemon and mods back // Make held items show up again
globalScene.getPlayerParty().push(playerPokemon); globalScene.updateItems(true);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
globalScene.updateModifiers(true);
queueEncounterMessage(`${namespace}:option.1.finished`); queueEncounterMessage(`${namespace}:option.1.finished`);
}; };
@ -217,8 +209,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
// Spawn medium training session with chosen pokemon // Spawn medium training session with chosen pokemon
// Every 40 waves, add +1 boss segment, capping at 6 // Every 40 waves, add +1 boss segment, capping at 6
const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 40), 6); const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 40), 6);
const modifiers = new ModifiersHolder(); const config = getEnemyConfig(playerPokemon, segments);
const config = getEnemyConfig(playerPokemon, segments, modifiers);
globalScene.removePokemonFromPlayerParty(playerPokemon, false); globalScene.removePokemonFromPlayerParty(playerPokemon, false);
const onBeforeRewardsPhase = () => { const onBeforeRewardsPhase = () => {
@ -227,13 +218,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
playerPokemon.setCustomNature(encounter.misc.chosenNature); playerPokemon.setCustomNature(encounter.misc.chosenNature);
globalScene.gameData.unlockSpeciesNature(playerPokemon.species, encounter.misc.chosenNature); globalScene.gameData.unlockSpeciesNature(playerPokemon.species, encounter.misc.chosenNature);
// Add pokemon and modifiers back // Make held items show up again
globalScene.getPlayerParty().push(playerPokemon); globalScene.updateItems(true);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
globalScene.updateModifiers(true);
}; };
setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase);
@ -308,8 +294,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
// Every 30 waves, add +1 boss segment, capping at 6 // Every 30 waves, add +1 boss segment, capping at 6
// Also starts with +1 to all stats // Also starts with +1 to all stats
const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 30), 6); const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 30), 6);
const modifiers = new ModifiersHolder(); const config = getEnemyConfig(playerPokemon, segments);
const config = getEnemyConfig(playerPokemon, segments, modifiers);
config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
globalScene.removePokemonFromPlayerParty(playerPokemon, false); globalScene.removePokemonFromPlayerParty(playerPokemon, false);
@ -340,13 +325,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
playerPokemon.calculateStats(); playerPokemon.calculateStats();
globalScene.gameData.setPokemonCaught(playerPokemon, false); globalScene.gameData.setPokemonCaught(playerPokemon, false);
// Add pokemon and mods back // Make held items show up again
globalScene.getPlayerParty().push(playerPokemon); globalScene.updateItems(true);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
globalScene.updateModifiers(true);
}; };
setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase);
@ -373,18 +353,12 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
) )
.build(); .build();
function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number, modifiers: ModifiersHolder): EnemyPartyConfig { function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number): EnemyPartyConfig {
playerPokemon.resetSummonData(); playerPokemon.resetSummonData();
// Passes modifiers by reference // Passes modifiers by reference
modifiers.value = playerPokemon.getHeldItems(); // TODO: fix various things, like make enemy items untransferable, make sure form change items can come back
const modifierConfigs = modifiers.value.map(mod => { const config = playerPokemon.heldItemManager.generateHeldItemConfiguration();
return {
modifier: mod.clone(),
isTransferable: false,
stackCount: mod.stackCount,
};
}) as HeldModifierConfig[];
const data = new PokemonData(playerPokemon); const data = new PokemonData(playerPokemon);
return { return {
@ -396,12 +370,8 @@ function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number, modifier
formIndex: playerPokemon.formIndex, formIndex: playerPokemon.formIndex,
level: playerPokemon.level, level: playerPokemon.level,
dataSource: data, dataSource: data,
modifierConfigs: modifierConfigs, heldItemConfig: config,
}, },
], ],
}; };
} }
class ModifiersHolder {
public value: PokemonHeldItemModifier[] = [];
}

View File

@ -1,14 +1,12 @@
import type { EnemyPartyConfig, EnemyPokemonConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
generateModifierType, assignItemToFirstFreePokemon,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
loadCustomMovesForEncounter, loadCustomMovesForEncounter,
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
@ -17,11 +15,9 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#app/modifier/modifier";
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
@ -29,6 +25,10 @@ import { PokemonMove } from "#app/data/moves/pokemon-move";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { allHeldItems } from "#app/data/data-lists";
import { TrainerItemId } from "#enums/trainer-item-id";
import { allTrainerItems } from "#app/data/data-lists";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/trashToTreasure"; const namespace = "mysteryEncounters/trashToTreasure";
@ -83,41 +83,13 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
formIndex: 1, // Gmax formIndex: 1, // Gmax
bossSegmentModifier: 1, // +1 Segment from normal bossSegmentModifier: 1, // +1 Segment from normal
moveSet: [MoveId.GUNK_SHOT, MoveId.STOMPING_TANTRUM, MoveId.HAMMER_ARM, MoveId.PAYBACK], moveSet: [MoveId.GUNK_SHOT, MoveId.STOMPING_TANTRUM, MoveId.HAMMER_ARM, MoveId.PAYBACK],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemCategoryId.BERRY, count: 4 },
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, { entry: HeldItemCategoryId.BASE_STAT_BOOST, count: 2 },
}, { entry: HeldItemId.TOXIC_ORB, count: randSeedInt(2, 0) },
{ { entry: HeldItemId.SOOTHE_BELL, count: randSeedInt(2, 1) },
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, { entry: HeldItemId.LUCKY_EGG, count: randSeedInt(3, 1) },
}, { entry: HeldItemId.GOLDEN_EGG, count: randSeedInt(2, 0) },
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
{
modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 1),
},
{
modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType,
stackCount: randSeedInt(3, 1),
},
{
modifier: generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
], ],
}; };
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
@ -157,18 +129,14 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
await transitionMysteryEncounterIntroVisuals(); await transitionMysteryEncounterIntroVisuals();
await tryApplyDigRewardItems(); await tryApplyDigRewardItems();
const blackSludge = generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ const blackSludge = globalScene.trainerItems.add(TrainerItemId.BLACK_SLUDGE);
SHOP_ITEM_COST_MULTIPLIER, if (blackSludge) {
]);
const modifier = blackSludge?.newModifier();
if (modifier) {
await globalScene.addModifier(modifier, false, false, false, true);
globalScene.playSound("battle_anims/PRSFX- Venom Drench", { globalScene.playSound("battle_anims/PRSFX- Venom Drench", {
volume: 2, volume: 2,
}); });
await showEncounterText( await showEncounterText(
i18next.t("battle:rewardGain", { i18next.t("battle:rewardGain", {
modifierName: modifier.type.name, modifierName: allTrainerItems[TrainerItemId.BLACK_SLUDGE].name,
}), }),
null, null,
undefined, undefined,
@ -200,7 +168,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], guaranteedModifierTiers: [RewardTier.ROGUE, RewardTier.ROGUE, RewardTier.ULTRA, RewardTier.GREAT],
fillRemaining: true, fillRemaining: true,
}); });
encounter.startOfBattleEffects.push( encounter.startOfBattleEffects.push(
@ -224,44 +192,18 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
.build(); .build();
async function tryApplyDigRewardItems() { async function tryApplyDigRewardItems() {
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType;
const leftovers = generateModifierType(modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType;
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
// Iterate over the party until an item was successfully given
// First leftovers // First leftovers
for (const pokemon of party) { assignItemToFirstFreePokemon(HeldItemId.LEFTOVERS, party);
const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
true,
) as PokemonHeldItemModifier[];
const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier;
if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount()) {
await applyModifierTypeToPlayerPokemon(pokemon, leftovers);
break;
}
}
// Second leftovers // Second leftovers
for (const pokemon of party) { assignItemToFirstFreePokemon(HeldItemId.LEFTOVERS, party);
const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
true,
) as PokemonHeldItemModifier[];
const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier;
if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount()) {
await applyModifierTypeToPlayerPokemon(pokemon, leftovers);
break;
}
}
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
await showEncounterText( await showEncounterText(
i18next.t("battle:rewardGainCount", { i18next.t("battle:rewardGainCount", {
modifierName: leftovers.name, modifierName: allHeldItems[HeldItemId.LEFTOVERS].name,
count: 2, count: 2,
}), }),
null, null,
@ -270,23 +212,12 @@ async function tryApplyDigRewardItems() {
); );
// Only Shell bell // Only Shell bell
for (const pokemon of party) { assignItemToFirstFreePokemon(HeldItemId.SHELL_BELL, party);
const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
true,
) as PokemonHeldItemModifier[];
const existingShellBell = heldItems.find(m => m instanceof HitHealModifier) as HitHealModifier;
if (!existingShellBell || existingShellBell.getStackCount() < existingShellBell.getMaxStackCount()) {
await applyModifierTypeToPlayerPokemon(pokemon, shellBell);
break;
}
}
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
await showEncounterText( await showEncounterText(
i18next.t("battle:rewardGainCount", { i18next.t("battle:rewardGainCount", {
modifierName: shellBell.name, modifierName: allHeldItems[HeldItemId.SHELL_BELL].name,
count: 1, count: 1,
}), }),
null, null,

View File

@ -1,6 +1,7 @@
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
getPartyBerries,
getRandomEncounterSpecies, getRandomEncounterSpecies,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
@ -15,10 +16,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { import { HeldItemRequirement, MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
MoveRequirement,
PersistentModifierRequirement,
} from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { import {
@ -33,10 +31,11 @@ import { BattlerIndex } from "#enums/battler-index";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { BerryModifier } from "#app/modifier/modifier";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import type { PokemonItemMap } from "#app/items/held-item-data-types";
import { HeldItemCategoryId } from "#enums/held-item-id";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/uncommonBreed"; const namespace = "mysteryEncounters/uncommonBreed";
@ -191,7 +190,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder.
) )
.withOption( .withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically .withSceneRequirement(new HeldItemRequirement(HeldItemCategoryId.BERRY, 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`, buttonTooltip: `${namespace}:option.2.tooltip`,
@ -207,19 +206,18 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder.
// Remove 4 random berries from player's party // Remove 4 random berries from player's party
// Get all player berry items, remove from party, and store reference // Get all player berry items, remove from party, and store reference
const berryItems: BerryModifier[] = globalScene.findModifiers(
m => m instanceof BerryModifier, const berryMap = getPartyBerries();
) as BerryModifier[]; const stolenBerryMap: PokemonItemMap[] = [];
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const index = randSeedInt(berryItems.length); const index = randSeedInt(berryMap.length);
const randBerry = berryItems[index]; const randBerry = berryMap[index];
randBerry.stackCount--; globalScene.getPokemonById(randBerry.pokemonId)?.heldItemManager.remove(randBerry.item.id);
if (randBerry.stackCount === 0) { stolenBerryMap.push(randBerry);
globalScene.removeModifier(randBerry); berryMap.splice(index, 1);
berryItems.splice(index, 1);
}
} }
await globalScene.updateModifiers(true, true); await globalScene.updateItems(true);
// Pokemon joins the team, with 2 egg moves // Pokemon joins the team, with 2 egg moves
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;

View File

@ -7,7 +7,6 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import type { EnemyPartyConfig, EnemyPokemonConfig } from "../utils/encounter-phase-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "../utils/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterRewards, setEncounterRewards,
@ -21,11 +20,8 @@ import { NumberHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { allSpecies } from "#app/data/data-lists"; import { allSpecies } from "#app/data/data-lists";
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import { import {
@ -34,15 +30,18 @@ import {
} from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { getLevelTotalExp } from "#app/data/exp"; import { getLevelTotalExp } from "#app/data/exp";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import PokemonData from "#app/system/pokemon-data"; import PokemonData from "#app/system/pokemon-data";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import type HeldModifierConfig from "#app/@types/held-modifier-config";
import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { trainerConfigs } from "#app/data/trainers/trainer-config";
import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import type { HeldItemConfiguration, HeldItemSpecs } from "#app/items/held-item-data-types";
import { assignItemsFromConfiguration } from "#app/items/held-item-pool";
import { HeldItemId } from "#enums/held-item-id";
import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
const namespace = "mysteryEncounters/weirdDream"; const namespace = "mysteryEncounters/weirdDream";
@ -262,20 +261,14 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
dataSource.player = false; dataSource.player = false;
// Copy held items to new pokemon // Copy held items to new pokemon
const newPokemonHeldItemConfigs: HeldModifierConfig[] = []; // TODO: Make items untransferable
for (const item of transformation.heldItems) { const newPokemonHeldItemConfig = transformation.heldItems;
newPokemonHeldItemConfigs.push({
modifier: item.clone() as PokemonHeldItemModifier,
stackCount: item.getStackCount(),
isTransferable: false,
});
}
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
newPokemonHeldItemConfigs.push({ newPokemonHeldItemConfig.push({
modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU) as PokemonHeldItemModifierType, entry: HeldItemId.OLD_GATEAU,
stackCount: 1, count: 1,
isTransferable: false,
}); });
} }
@ -284,7 +277,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD, isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD,
level: previousPokemon.level, level: previousPokemon.level,
dataSource: dataSource, dataSource: dataSource,
modifierConfigs: newPokemonHeldItemConfigs, heldItemConfig: newPokemonHeldItemConfig,
}; };
enemyPokemonConfigs.push(enemyConfig); enemyPokemonConfigs.push(enemyConfig);
@ -316,12 +309,12 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ROGUE, RewardTier.ROGUE,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.ULTRA, RewardTier.ULTRA,
ModifierTier.GREAT, RewardTier.GREAT,
ModifierTier.GREAT, RewardTier.GREAT,
], ],
fillRemaining: false, fillRemaining: false,
}, },
@ -365,7 +358,7 @@ interface PokemonTransformation {
previousPokemon: PlayerPokemon; previousPokemon: PlayerPokemon;
newSpecies: PokemonSpecies; newSpecies: PokemonSpecies;
newPokemon: PlayerPokemon; newPokemon: PlayerPokemon;
heldItems: PokemonHeldItemModifier[]; heldItems: HeldItemConfiguration;
} }
function getTeamTransformations(): PokemonTransformation[] { function getTeamTransformations(): PokemonTransformation[] {
@ -390,9 +383,7 @@ function getTeamTransformations(): PokemonTransformation[] {
for (let i = 0; i < numPokemon; i++) { for (let i = 0; i < numPokemon; i++) {
const removed = removedPokemon[i]; const removed = removedPokemon[i];
const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id);
pokemonTransformations[index].heldItems = removed pokemonTransformations[index].heldItems = removed.heldItemManager.generateHeldItemConfiguration();
.getHeldItems()
.filter(m => !(m instanceof PokemonFormChangeItemModifier));
const bst = removed.getSpeciesForm().getBaseStatTotal(); const bst = removed.getSpeciesForm().getBaseStatTotal();
let newBstRange: [number, number]; let newBstRange: [number, number];
@ -448,17 +439,14 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
} }
// Copy old items to new pokemon // Copy old items to new pokemon
for (const item of transformation.heldItems) { const heldItemConfiguration = transformation.heldItems;
item.pokemonId = newPokemon.id;
globalScene.addModifier(item, false, false, false, true);
}
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU(); heldItemConfiguration.push({
const modifier = modType?.newModifier(newPokemon); entry: HeldItemId.OLD_GATEAU,
if (modifier) { count: 1,
globalScene.addModifier(modifier, false, false, false, true); });
}
} }
newPokemon.calculateStats(); newPokemon.calculateStats();
@ -499,7 +487,9 @@ async function postProcessTransformedPokemon(
const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; const hiddenIndex = newPokemon.species.ability2 ? 2 : 1;
if (newPokemon.abilityIndex < hiddenIndex) { if (newPokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new NumberHolder(256); const hiddenAbilityChance = new NumberHolder(256);
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);

View File

@ -8,14 +8,14 @@ import { StatusEffect } from "#enums/status-effect";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import { AttackTypeBoosterModifier } from "#app/modifier/modifier";
import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
import { coerceArray, isNullOrUndefined } from "#app/utils/common"; import { coerceArray, isNullOrUndefined } from "#app/utils/common";
import type { AbilityId } from "#enums/ability-id"; import type { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import type { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { allHeldItems } from "#app/data/data-lists";
export interface EncounterRequirement { export interface EncounterRequirement {
meetsRequirement(): boolean; // Boolean to see if a requirement is met meetsRequirement(): boolean; // Boolean to see if a requirement is met
@ -351,39 +351,6 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
} }
} }
export class PersistentModifierRequirement extends EncounterSceneRequirement {
requiredHeldItemModifiers: string[];
minNumberOfItems: number;
constructor(heldItem: string | string[], minNumberOfItems = 1) {
super();
this.minNumberOfItems = minNumberOfItems;
this.requiredHeldItemModifiers = coerceArray(heldItem);
}
override meetsRequirement(): boolean {
const partyPokemon = globalScene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) {
return false;
}
let modifierCount = 0;
for (const modifier of this.requiredHeldItemModifiers) {
const matchingMods = globalScene.findModifiers(m => m.constructor.name === modifier);
if (matchingMods?.length > 0) {
for (const matchingMod of matchingMods) {
modifierCount += matchingMod.stackCount;
}
}
}
return modifierCount >= this.minNumberOfItems;
}
override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] {
return ["requiredItem", this.requiredHeldItemModifiers[0]];
}
}
export class MoneyRequirement extends EncounterSceneRequirement { export class MoneyRequirement extends EncounterSceneRequirement {
requiredMoney: number; // Static value requiredMoney: number; // Static value
scalingMultiplier: number; // Calculates required money based off wave index scalingMultiplier: number; // Calculates required money based off wave index
@ -833,72 +800,13 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen
} }
export class HeldItemRequirement extends EncounterPokemonRequirement { export class HeldItemRequirement extends EncounterPokemonRequirement {
requiredHeldItemModifiers: string[]; requiredHeldItems: HeldItemId[] | HeldItemCategoryId[];
minNumberOfPokemon: number;
invertQuery: boolean;
requireTransferable: boolean;
constructor(heldItem: string | string[], minNumberOfPokemon = 1, invertQuery = false, requireTransferable = true) {
super();
this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery;
this.requiredHeldItemModifiers = coerceArray(heldItem);
this.requireTransferable = requireTransferable;
}
override meetsRequirement(): boolean {
const partyPokemon = globalScene.getPlayerParty();
if (isNullOrUndefined(partyPokemon)) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
}
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) {
return partyPokemon.filter(pokemon =>
this.requiredHeldItemModifiers.some(heldItem => {
return pokemon.getHeldItems().some(it => {
return it.constructor.name === heldItem && (!this.requireTransferable || it.isTransferable);
});
}),
);
}
// for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers
// E.g. functions as a blacklist
return partyPokemon.filter(
pokemon =>
pokemon.getHeldItems().filter(it => {
return (
!this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem) &&
(!this.requireTransferable || it.isTransferable)
);
}).length > 0,
);
}
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
const requiredItems = pokemon?.getHeldItems().filter(it => {
return (
this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem) &&
(!this.requireTransferable || it.isTransferable)
);
});
if (requiredItems && requiredItems.length > 0) {
return ["heldItem", requiredItems[0].type.name];
}
return ["heldItem", ""];
}
}
export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRequirement {
requiredHeldItemTypes: PokemonType[];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; invertQuery: boolean;
requireTransferable: boolean; requireTransferable: boolean;
constructor( constructor(
heldItemTypes: PokemonType | PokemonType[], heldItem: HeldItemId | HeldItemId[] | HeldItemCategoryId | HeldItemCategoryId[],
minNumberOfPokemon = 1, minNumberOfPokemon = 1,
invertQuery = false, invertQuery = false,
requireTransferable = true, requireTransferable = true,
@ -906,7 +814,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
super(); super();
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredHeldItemTypes = coerceArray(heldItemTypes); this.requiredHeldItems = coerceArray(heldItem);
this.requireTransferable = requireTransferable; this.requireTransferable = requireTransferable;
} }
@ -921,14 +829,9 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) { if (!this.invertQuery) {
return partyPokemon.filter(pokemon => return partyPokemon.filter(pokemon =>
this.requiredHeldItemTypes.some(heldItemType => { this.requiredHeldItems.some(heldItem => {
return pokemon.getHeldItems().some(it => { (pokemon.heldItemManager.hasItem(heldItem) || pokemon.heldItemManager.hasItemCategory(heldItem)) &&
return ( (!this.requireTransferable || allHeldItems[heldItem].isTransferable);
it instanceof AttackTypeBoosterModifier &&
(it.type as AttackTypeBoosterModifierType).moveType === heldItemType &&
(!this.requireTransferable || it.isTransferable)
);
});
}), }),
); );
} }
@ -936,30 +839,24 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
// E.g. functions as a blacklist // E.g. functions as a blacklist
return partyPokemon.filter( return partyPokemon.filter(
pokemon => pokemon =>
pokemon.getHeldItems().filter(it => { pokemon.getHeldItems().filter(item => {
return !this.requiredHeldItemTypes.some( return (
heldItemType => !this.requiredHeldItems.some(heldItem => item === heldItem) &&
it instanceof AttackTypeBoosterModifier && (!this.requireTransferable || allHeldItems[item].isTransferable)
(it.type as AttackTypeBoosterModifierType).moveType === heldItemType &&
(!this.requireTransferable || it.isTransferable),
); );
}).length > 0, }).length > 0,
); );
} }
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
const requiredItems = pokemon?.getHeldItems().filter(it => { const requiredItems = pokemon?.getHeldItems().filter(item => {
return ( return (
this.requiredHeldItemTypes.some( this.requiredHeldItems.some(heldItem => item === heldItem) &&
heldItemType => (!this.requireTransferable || allHeldItems[item].isTransferable)
it instanceof AttackTypeBoosterModifier &&
(it.type as AttackTypeBoosterModifierType).moveType === heldItemType,
) &&
(!this.requireTransferable || it.isTransferable)
); );
}); });
if (requiredItems && requiredItems.length > 0) { if (requiredItems && requiredItems.length > 0) {
return ["heldItem", requiredItems[0].type.name]; return ["heldItem", allHeldItems[requiredItems[0]].name];
} }
return ["heldItem", ""]; return ["heldItem", ""];
} }

View File

@ -1,5 +1,4 @@
import type Battle from "#app/battle"; import type Battle from "#app/battle";
import { BattleType } from "#enums/battle-type";
import { biomeLinks, BiomePoolTier } from "#app/data/balance/biomes"; import { biomeLinks, BiomePoolTier } from "#app/data/balance/biomes";
import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
import { AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/constants"; import { AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/constants";
@ -11,12 +10,7 @@ import { EnemyPokemon } from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import type { CustomModifierSettings, ModifierType } from "#app/modifier/modifier-type"; import type { CustomModifierSettings, ModifierType } from "#app/modifier/modifier-type";
import { import { getPartyLuckValue, ModifierTypeGenerator, ModifierTypeOption } from "#app/modifier/modifier-type";
getPartyLuckValue,
ModifierTypeGenerator,
ModifierTypeOption,
regenerateModifierPoolThresholds,
} from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
import type PokemonData from "#app/system/pokemon-data"; import type PokemonData from "#app/system/pokemon-data";
@ -44,7 +38,6 @@ import type PokemonSpecies from "#app/data/pokemon-species";
import type { IEggOptions } from "#app/data/egg"; import type { IEggOptions } from "#app/data/egg";
import { Egg } from "#app/data/egg"; import { Egg } from "#app/data/egg";
import type { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; import type { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import type HeldModifierConfig from "#app/@types/held-modifier-config";
import type { Variant } from "#app/sprites/variant"; import type { Variant } from "#app/sprites/variant";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
@ -53,6 +46,9 @@ import { PokemonType } from "#enums/pokemon-type";
import { getNatureName } from "#app/data/nature"; import { getNatureName } from "#app/data/nature";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import type { HeldItemConfiguration, PokemonItemMap } from "#app/items/held-item-data-types";
import { HeldItemCategoryId, type HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { allHeldItems } from "#app/data/data-lists";
/** /**
* Animates exclamation sprite over trainer's head at start of encounter * Animates exclamation sprite over trainer's head at start of encounter
@ -102,7 +98,7 @@ export interface EnemyPokemonConfig {
/** Can set just the status, or pass a timer on the status turns */ /** Can set just the status, or pass a timer on the status turns */
status?: StatusEffect | [StatusEffect, number]; status?: StatusEffect | [StatusEffect, number];
mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
modifierConfigs?: HeldModifierConfig[]; heldItemConfig?: HeldItemConfiguration;
tags?: BattlerTagType[]; tags?: BattlerTagType[];
dataSource?: PokemonData; dataSource?: PokemonData;
tera?: PokemonType; tera?: PokemonType;
@ -200,6 +196,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
battle.enemyLevels.forEach((level, e) => { battle.enemyLevels.forEach((level, e) => {
let enemySpecies: PokemonSpecies | undefined; let enemySpecies: PokemonSpecies | undefined;
let heldItemConfig: HeldItemConfiguration = [];
let dataSource: PokemonData | undefined; let dataSource: PokemonData | undefined;
let isBoss = false; let isBoss = false;
if (!loaded) { if (!loaded) {
@ -211,12 +208,14 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
dataSource = config.dataSource; dataSource = config.dataSource;
enemySpecies = config.species; enemySpecies = config.species;
isBoss = config.isBoss; isBoss = config.isBoss;
heldItemConfig = config.heldItemConfig ?? [];
battle.enemyParty[e] = globalScene.addEnemyPokemon( battle.enemyParty[e] = globalScene.addEnemyPokemon(
enemySpecies, enemySpecies,
level, level,
TrainerSlot.TRAINER, TrainerSlot.TRAINER,
isBoss, isBoss,
false, false,
heldItemConfig,
dataSource, dataSource,
); );
} else { } else {
@ -226,6 +225,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) {
const config = partyConfig.pokemonConfigs[e]; const config = partyConfig.pokemonConfigs[e];
level = config.level ? config.level : level; level = config.level ? config.level : level;
heldItemConfig = config.heldItemConfig ?? [];
dataSource = config.dataSource; dataSource = config.dataSource;
enemySpecies = config.species; enemySpecies = config.species;
isBoss = config.isBoss; isBoss = config.isBoss;
@ -242,6 +242,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
TrainerSlot.NONE, TrainerSlot.NONE,
isBoss, isBoss,
false, false,
heldItemConfig,
dataSource, dataSource,
); );
} }
@ -428,16 +429,6 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
enemyPokemon_2.x += 300; enemyPokemon_2.x += 300;
} }
}); });
if (!loaded) {
regenerateModifierPoolThresholds(
globalScene.getEnemyField(),
battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD,
);
const customModifierTypes = partyConfig?.pokemonConfigs
?.filter(config => config?.modifierConfigs)
.map(config => config.modifierConfigs!);
globalScene.generateEnemyModifiers(customModifierTypes);
}
} }
/** /**
@ -1305,3 +1296,29 @@ export function calculateRareSpawnAggregateStats(luckValue: number) {
console.log(stats); console.log(stats);
} }
// Iterate over the party until an item is successfully given
export function assignItemToFirstFreePokemon(item: HeldItemId, party: Pokemon[]): void {
for (const pokemon of party) {
const stack = pokemon.heldItemManager.getStack(item);
if (stack < allHeldItems[item].getMaxStackCount()) {
pokemon.heldItemManager.add(item);
return;
}
}
}
// Creates an item map of berries to pokemon, storing each berry separately (splitting up stacks)
export function getPartyBerries(): PokemonItemMap[] {
const pokemonItems: PokemonItemMap[] = [];
globalScene.getPlayerParty().forEach(pokemon => {
const berries = pokemon.getHeldItems().filter(item => isItemInCategory(item, HeldItemCategoryId.BERRY));
berries.forEach(berryId => {
const berryStack = pokemon.heldItemManager.getStack(berryId);
for (let i = 1; i <= berryStack; i++) {
pokemonItems.push({ item: { id: berryId, stack: 1 }, pokemonId: pokemon.id });
}
});
});
return pokemonItems;
}

View File

@ -1,7 +1,6 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import i18next from "i18next"; import i18next from "i18next";
import { isNullOrUndefined, randSeedInt } from "#app/utils/common"; import { isNullOrUndefined, randSeedInt } from "#app/utils/common";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { import {
@ -28,8 +27,6 @@ import {
showEncounterText, showEncounterText,
} from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import type { PermanentStat } from "#enums/stat"; import type { PermanentStat } from "#enums/stat";
import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler";
@ -37,6 +34,7 @@ import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import type { AbilityId } from "#enums/ability-id"; import type { AbilityId } from "#enums/ability-id";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import type { HeldItemId } from "#enums/held-item-id";
/** Will give +1 level every 10 waves */ /** Will give +1 level every 10 waves */
export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1;
@ -369,60 +367,13 @@ export function applyHealToPokemon(pokemon: PlayerPokemon, heal: number) {
applyHpChangeToPokemon(pokemon, heal); applyHpChangeToPokemon(pokemon, heal);
} }
/** export function applyHeldItemWithFallback(pokemon: Pokemon, item: HeldItemId, fallbackItem?: HeldItemId) {
* Will modify all of a Pokemon's base stats by a flat value const added = pokemon.heldItemManager.add(item);
* Base stats can never go below 1 if (!added && fallbackItem) {
* @param pokemon pokemon.heldItemManager.add(fallbackItem);
* @param value
*/
export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, good: boolean) {
const modType = modifierTypes
.MYSTERY_ENCOUNTER_SHUCKLE_JUICE()
.generateType(globalScene.getPlayerParty(), [good ? 10 : -15])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE);
const modifier = modType?.newModifier(pokemon);
if (modifier) {
globalScene.addModifier(modifier, false, false, false, true);
pokemon.calculateStats();
} }
} }
/**
* Will attempt to add a new modifier to a Pokemon.
* If the Pokemon already has max stacks of that item, it will instead apply 'fallbackModifierType', if specified.
* @param scene
* @param pokemon
* @param modType
* @param fallbackModifierType
*/
export async function applyModifierTypeToPlayerPokemon(
pokemon: PlayerPokemon,
modType: PokemonHeldItemModifierType,
fallbackModifierType?: PokemonHeldItemModifierType,
) {
// Check if the Pokemon has max stacks of that item already
const modifier = modType.newModifier(pokemon);
const existing = globalScene.findModifier(
m =>
m instanceof PokemonHeldItemModifier &&
m.type.id === modType.id &&
m.pokemonId === pokemon.id &&
m.matchType(modifier),
) as PokemonHeldItemModifier;
// At max stacks
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) {
if (!fallbackModifierType) {
return;
}
// Apply fallback
return applyModifierTypeToPlayerPokemon(pokemon, fallbackModifierType);
}
globalScene.addModifier(modifier, false, false, false, true);
}
/** /**
* Alternative to using AttemptCapturePhase * Alternative to using AttemptCapturePhase
* Assumes player sprite is visible on the screen (this is intended for non-combat uses) * Assumes player sprite is visible on the screen (this is intended for non-combat uses)
@ -687,20 +638,10 @@ export async function catchPokemon(
} }
}; };
const addToParty = (slotIndex?: number) => { const addToParty = (slotIndex?: number) => {
const newPokemon = pokemon.addToParty(pokeballType, slotIndex); pokemon.addToParty(pokeballType, slotIndex);
const modifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === 6) { if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === 6) {
globalScene.validateAchv(achvs.SHINY_PARTY); globalScene.validateAchv(achvs.SHINY_PARTY);
} }
Promise.all(modifiers.map(m => globalScene.addModifier(m, true))).then(() => {
globalScene.updateModifiers(true);
removePokemon();
if (newPokemon) {
newPokemon.loadAssets().then(end);
} else {
end();
}
});
}; };
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
if (globalScene.getPlayerParty().length === 6) { if (globalScene.getPlayerParty().length === 6) {
@ -725,6 +666,7 @@ export async function catchPokemon(
pokemon.variant, pokemon.variant,
pokemon.ivs, pokemon.ivs,
pokemon.nature, pokemon.nature,
pokemon.heldItemManager.generateHeldItemConfiguration(),
pokemon, pokemon,
); );
globalScene.ui.setMode( globalScene.ui.setMode(

View File

@ -1,4 +1,5 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item";
import { NumberHolder } from "#app/utils/common"; import { NumberHolder } from "#app/utils/common";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import i18next from "i18next"; import i18next from "i18next";
@ -93,7 +94,9 @@ export function getCriticalCaptureChance(modifiedCatchRate: number): number {
} }
const dexCount = globalScene.gameData.getSpeciesCount(d => !!d.caughtAttr); const dexCount = globalScene.gameData.getSpeciesCount(d => !!d.caughtAttr);
const catchingCharmMultiplier = new NumberHolder(1); const catchingCharmMultiplier = new NumberHolder(1);
globalScene.findModifier(m => m.is("CriticalCatchChanceBoosterModifier"))?.apply(catchingCharmMultiplier); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.CRITICAL_CATCH_CHANCE_BOOSTER, {
numberHolder: catchingCharmMultiplier,
});
const dexMultiplier = const dexMultiplier =
globalScene.gameMode.isDaily || dexCount > 800 globalScene.gameMode.isDaily || dexCount > 800
? 2.5 ? 2.5

View File

@ -25,6 +25,11 @@ import {
type SpeciesFormChangeTrigger, type SpeciesFormChangeTrigger,
SpeciesFormChangeWeatherTrigger, SpeciesFormChangeWeatherTrigger,
} from "./pokemon-forms/form-change-triggers"; } from "./pokemon-forms/form-change-triggers";
import i18next from "i18next";
export function formChangeItemName(id: FormChangeItem) {
return i18next.t(`modifierType:FormChangeItem.${FormChangeItem[id]}`);
}
export type SpeciesFormChangeConditionPredicate = (p: Pokemon) => boolean; export type SpeciesFormChangeConditionPredicate = (p: Pokemon) => boolean;
export type SpeciesFormChangeConditionEnforceFunc = (p: Pokemon) => void; export type SpeciesFormChangeConditionEnforceFunc = (p: Pokemon) => void;

View File

@ -3,7 +3,6 @@ import { coerceArray, type Constructor } from "#app/utils/common";
import type { TimeOfDay } from "#enums/time-of-day"; import type { TimeOfDay } from "#enums/time-of-day";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import type { SpeciesFormChange } from "#app/data/pokemon-forms"; import type { SpeciesFormChange } from "#app/data/pokemon-forms";
import type { PokemonFormChangeItemModifier } from "#app/modifier/modifier";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { FormChangeItem } from "#enums/form-change-item"; import { FormChangeItem } from "#enums/form-change-item";
@ -77,16 +76,12 @@ export class SpeciesFormChangeItemTrigger extends SpeciesFormChangeTrigger {
} }
canChange(pokemon: Pokemon): boolean { canChange(pokemon: Pokemon): boolean {
return !!globalScene.findModifier(r => { const matchItem = pokemon.heldItemManager.formChangeItems[this.item];
// Assume that if m has the `formChangeItem` property, then it is a PokemonFormChangeItemModifier if (!matchItem) {
const m = r as PokemonFormChangeItemModifier; return false;
return ( }
"formChangeItem" in m && console.log("CAN CHANGE FORMS:", matchItem.active === this.active);
m.pokemonId === pokemon.id && return matchItem.active === this.active;
m.formChangeItem === this.item &&
m.active === this.active
);
});
} }
} }

View File

@ -49,7 +49,7 @@ import type { EnemyPokemon } from "#app/field/pokemon";
import type { EvilTeam } from "./evil-admin-trainer-pools"; import type { EvilTeam } from "./evil-admin-trainer-pools";
import type { import type {
PartyMemberFunc, PartyMemberFunc,
GenModifiersFunc, GenTrainerItemsFunc,
GenAIFunc, GenAIFunc,
PartyTemplateFunc, PartyTemplateFunc,
TrainerTierPools, TrainerTierPools,
@ -118,7 +118,7 @@ export class TrainerConfig {
public femaleEncounterBgm: string; public femaleEncounterBgm: string;
public doubleEncounterBgm: string; public doubleEncounterBgm: string;
public victoryBgm: string; public victoryBgm: string;
public genModifiersFunc: GenModifiersFunc; public genModifiersFunc: GenTrainerItemsFunc;
public genAIFuncs: GenAIFunc[] = []; public genAIFuncs: GenAIFunc[] = [];
public modifierRewardFuncs: ModifierTypeFunc[] = []; public modifierRewardFuncs: ModifierTypeFunc[] = [];
public partyTemplates: TrainerPartyTemplate[]; public partyTemplates: TrainerPartyTemplate[];
@ -470,7 +470,7 @@ export class TrainerConfig {
return this; return this;
} }
setGenModifiersFunc(genModifiersFunc: GenModifiersFunc): TrainerConfig { setGenModifiersFunc(genModifiersFunc: GenTrainerItemsFunc): TrainerConfig {
this.genModifiersFunc = genModifiersFunc; this.genModifiersFunc = genModifiersFunc;
return this; return this;
} }
@ -1004,6 +1004,7 @@ export function getRandomPartyMemberFunc(
undefined, undefined,
false, false,
undefined, undefined,
undefined,
postProcess, postProcess,
); );
}; };
@ -1028,7 +1029,16 @@ function getSpeciesFilterRandomPartyMemberFunc(
.getTrainerSpeciesForLevel(level, true, strength, waveIndex), .getTrainerSpeciesForLevel(level, true, strength, waveIndex),
); );
return globalScene.addEnemyPokemon(species, level, trainerSlot, undefined, false, undefined, postProcess); return globalScene.addEnemyPokemon(
species,
level,
trainerSlot,
undefined,
false,
undefined,
undefined,
postProcess,
);
}; };
} }

147
src/enums/held-item-id.ts Normal file
View File

@ -0,0 +1,147 @@
// TODO: make category the lower 2 bytes
export const HeldItemId = {
NONE: 0x0000,
// Berries
SITRUS_BERRY: 0x0101,
LUM_BERRY: 0x0102,
ENIGMA_BERRY: 0x0103,
LIECHI_BERRY: 0x0104,
GANLON_BERRY: 0x0105,
PETAYA_BERRY: 0x0106,
APICOT_BERRY: 0x0107,
SALAC_BERRY: 0x0108,
LANSAT_BERRY: 0x0109,
STARF_BERRY: 0x010A,
LEPPA_BERRY: 0x010B,
// Other items that are consumed
REVIVER_SEED: 0x0201,
WHITE_HERB: 0x0202,
// Type Boosters
SILK_SCARF: 0x0301,
BLACK_BELT: 0x0302,
SHARP_BEAK: 0x0303,
POISON_BARB: 0x0304,
SOFT_SAND: 0x0305,
HARD_STONE: 0x0306,
SILVER_POWDER: 0x0307,
SPELL_TAG: 0x0308,
METAL_COAT: 0x0309,
CHARCOAL: 0x030A,
MYSTIC_WATER: 0x030B,
MIRACLE_SEED: 0x030C,
MAGNET: 0x030D,
TWISTED_SPOON: 0x030E,
NEVER_MELT_ICE: 0x030F,
DRAGON_FANG: 0x0310,
BLACK_GLASSES: 0x0311,
FAIRY_FEATHER: 0x0312,
// Species Stat Boosters
LIGHT_BALL: 0x0401,
THICK_CLUB: 0x0402,
METAL_POWDER: 0x0403,
QUICK_POWDER: 0x0404,
DEEP_SEA_SCALE: 0x0405,
DEEP_SEA_TOOTH: 0x0406,
// Crit Boosters
SCOPE_LENS: 0x0501,
LEEK: 0x0502,
// Items increasing gains
LUCKY_EGG: 0x0601,
GOLDEN_EGG: 0x0602,
SOOTHE_BELL: 0x0603,
// Unique items
FOCUS_BAND: 0x0701,
QUICK_CLAW: 0x0702,
KINGS_ROCK: 0x0703,
LEFTOVERS: 0x0704,
SHELL_BELL: 0x0705,
MYSTICAL_ROCK: 0x0706,
WIDE_LENS: 0x0707,
MULTI_LENS: 0x0708,
GOLDEN_PUNCH: 0x0709,
GRIP_CLAW: 0x070A,
TOXIC_ORB: 0x070B,
FLAME_ORB: 0x070C,
SOUL_DEW: 0x070D,
BATON: 0x070E,
MINI_BLACK_HOLE: 0x070F,
EVIOLITE: 0x0710,
// Vitamins
HP_UP: 0x0801,
PROTEIN: 0x0802,
IRON: 0x0803,
CALCIUM: 0x0804,
ZINC: 0x0805,
CARBOS: 0x0806,
// Other stat boosting items
SHUCKLE_JUICE_GOOD: 0x0901,
SHUCKLE_JUICE_BAD: 0x0902,
OLD_GATEAU: 0x0903,
MACHO_BRACE: 0x0904,
// Evo trackers
GIMMIGHOUL_EVO_TRACKER: 0x0A01,
};
export type HeldItemId = (typeof HeldItemId)[keyof typeof HeldItemId];
type HeldItemName = keyof typeof HeldItemId;
type HeldItemValue = typeof HeldItemId[HeldItemName];
// Use a type-safe reducer to force number keys and values
export const HeldItemNames: Record<HeldItemValue, HeldItemName> = Object.entries(HeldItemId).reduce(
(acc, [key, value]) => {
acc[value as HeldItemValue] = key as HeldItemName;
return acc;
},
{} as Record<HeldItemValue, HeldItemName>
);
export const HeldItemCategoryId = {
NONE: 0x0000,
BERRY: 0x0100,
CONSUMABLE: 0x0200,
TYPE_ATTACK_BOOSTER: 0x0300,
SPECIES_STAT_BOOSTER: 0x0400,
CRIT_BOOSTER: 0x0500,
GAIN_INCREASE: 0x0600,
UNIQUE: 0x0700,
VITAMIN: 0x0800,
BASE_STAT_BOOST: 0x0900,
EVO_TRACKER: 0x0A00,
};
export type HeldItemCategoryId = (typeof HeldItemCategoryId)[keyof typeof HeldItemCategoryId];
const ITEM_CATEGORY_MASK = 0xFF00
export function getHeldItemCategory(itemId: HeldItemId): HeldItemCategoryId {
return itemId & ITEM_CATEGORY_MASK;
}
export function isCategoryId(categoryId: HeldItemCategoryId): boolean {
return (categoryId & ITEM_CATEGORY_MASK) === categoryId;
}
export function isItemInCategory(itemId: HeldItemId, category: HeldItemCategoryId): boolean {
return getHeldItemCategory(itemId) === category;
}
export function isItemInRequested(
itemId: HeldItemId,
requestedItems: (HeldItemCategoryId | HeldItemId)[]
): boolean {
return requestedItems.some(entry => {
itemId === entry || (itemId & ITEM_CATEGORY_MASK) === entry
});
}

View File

@ -1,7 +1,13 @@
export enum ModifierPoolType { export enum ModifierPoolType {
PLAYER, PLAYER,
}
export enum HeldItemPoolType {
WILD, WILD,
TRAINER, TRAINER,
ENEMY_BUFF, DAILY_STARTER,
DAILY_STARTER
} }
export enum TrainerItemPoolType {
ENEMY_BUFF,
}

View File

@ -1,4 +1,4 @@
export enum ModifierTier { export enum RewardTier {
COMMON, COMMON,
GREAT, GREAT,
ULTRA, ULTRA,

View File

@ -0,0 +1,68 @@
export const TrainerItemId = {
NONE: 0x0000,
MAP: 0x0B01,
IV_SCANNER: 0x0B02,
LOCK_CAPSULE: 0x0B03,
MEGA_BRACELET: 0x0B04,
DYNAMAX_BAND: 0x0B05,
TERA_ORB: 0x0B06,
GOLDEN_POKEBALL: 0x0B07,
OVAL_CHARM: 0x0B08,
EXP_SHARE: 0x0B09,
EXP_BALANCE: 0x0B0A,
CANDY_JAR: 0x0B0B,
BERRY_POUCH: 0x0B0C,
HEALING_CHARM: 0x0B0D,
EXP_CHARM: 0x0B0E,
SUPER_EXP_CHARM: 0x0B0F,
GOLDEN_EXP_CHARM: 0x0B10,
AMULET_COIN: 0x0B11,
ABILITY_CHARM: 0x0B12,
SHINY_CHARM: 0x0B13,
CATCHING_CHARM: 0x0B14,
BLACK_SLUDGE: 0x0B15,
GOLDEN_BUG_NET: 0x0B16,
LURE: 0x0C01,
SUPER_LURE: 0x0C02,
MAX_LURE: 0x0C03,
X_ATTACK: 0x0D01,
X_DEFENSE: 0x0D02,
X_SP_ATK: 0x0D03,
X_SP_DEF: 0x0D04,
X_SPEED: 0x0D05,
X_ACCURACY: 0x0D06,
DIRE_HIT: 0x0D07,
ENEMY_DAMAGE_BOOSTER: 0x0E01,
ENEMY_DAMAGE_REDUCTION: 0x0E02,
ENEMY_HEAL: 0x0E03,
ENEMY_ATTACK_POISON_CHANCE: 0x0E04,
ENEMY_ATTACK_PARALYZE_CHANCE: 0x0E05,
ENEMY_ATTACK_BURN_CHANCE: 0x0E06,
ENEMY_STATUS_EFFECT_HEAL_CHANCE: 0x0E07,
ENEMY_ENDURE_CHANCE: 0x0E08,
ENEMY_FUSED_CHANCE: 0x0E09,
};
export type TrainerItemId = (typeof TrainerItemId)[keyof typeof TrainerItemId];
type TrainerItemName = keyof typeof TrainerItemId;
type TrainerItemValue = typeof TrainerItemId[TrainerItemName];
// Use a type-safe reducer to force number keys and values
export const TrainerItemNames: Record<TrainerItemValue, TrainerItemName> = Object.entries(TrainerItemId).reduce(
(acc, [key, value]) => {
acc[value as TrainerItemValue] = key as TrainerItemName;
return acc;
},
{} as Record<TrainerItemValue, TrainerItemName>
);

View File

@ -1,5 +1,6 @@
import type Pokemon from "#app/field/pokemon";
import type { BerryType } from "#enums/berry-type";
import type Move from "../data/moves/move"; import type Move from "../data/moves/move";
import type { BerryModifier } from "../modifier/modifier";
/** Alias for all {@linkcode BattleScene} events */ /** Alias for all {@linkcode BattleScene} events */
export enum BattleSceneEventType { export enum BattleSceneEventType {
@ -81,12 +82,13 @@ export class MoveUsedEvent extends Event {
* @extends Event * @extends Event
*/ */
export class BerryUsedEvent extends Event { export class BerryUsedEvent extends Event {
/** The {@linkcode BerryModifier} being used */ /** The {@linkcode BerryType} being used */
public berryModifier: BerryModifier; public pokemon: Pokemon;
constructor(berry: BerryModifier) { public berryType: BerryType;
constructor(pokemon: Pokemon, berryType: BerryType) {
super(BattleSceneEventType.BERRY_USED); super(BattleSceneEventType.BERRY_USED);
this.pokemon = pokemon;
this.berryModifier = berry; this.berryType = berryType;
} }
} }

View File

@ -34,7 +34,8 @@ import {
SpeciesFormChangeWeatherTrigger, SpeciesFormChangeWeatherTrigger,
} from "#app/data/pokemon-forms/form-change-triggers"; } from "#app/data/pokemon-forms/form-change-triggers";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { FieldEffectModifier } from "#app/modifier/modifier"; import { applyHeldItems } from "#app/items/all-held-items";
import { HELD_ITEM_EFFECT } from "#app/items/held-item";
export class Arena { export class Arena {
public biomeType: BiomeId; public biomeType: BiomeId;
@ -338,7 +339,7 @@ export class Arena {
if (!isNullOrUndefined(user)) { if (!isNullOrUndefined(user)) {
weatherDuration.value = 5; weatherDuration.value = 5;
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); applyHeldItems(HELD_ITEM_EFFECT.FIELD_EFFECT, { pokemon: user, fieldDuration: weatherDuration });
} }
this.weather = weather ? new Weather(weather, weatherDuration.value) : null; this.weather = weather ? new Weather(weather, weatherDuration.value) : null;
@ -425,7 +426,7 @@ export class Arena {
if (!isNullOrUndefined(user)) { if (!isNullOrUndefined(user)) {
terrainDuration.value = 5; terrainDuration.value = 5;
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); applyHeldItems(HELD_ITEM_EFFECT.FIELD_EFFECT, { pokemon: user, fieldDuration: terrainDuration });
} }
this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null; this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null;

View File

@ -0,0 +1,229 @@
import { allHeldItems } from "#app/data/data-lists";
import { isItemInCategory, isItemInRequested, type HeldItemCategoryId, type HeldItemId } from "#app/enums/held-item-id";
import type { FormChangeItem } from "#enums/form-change-item";
import {
type HeldItemConfiguration,
isHeldItemSpecs,
type HeldItemDataMap,
type HeldItemSpecs,
type FormChangeItemPropertyMap,
type FormChangeItemSpecs,
type HeldItemSaveData,
} from "#app/items/held-item-data-types";
export class PokemonItemManager {
public heldItems: HeldItemDataMap;
public formChangeItems: FormChangeItemPropertyMap;
constructor() {
this.heldItems = {};
this.formChangeItems = {};
}
getItemSpecs(id: HeldItemId): HeldItemSpecs | undefined {
const item = this.heldItems[id];
if (item) {
const itemSpecs: HeldItemSpecs = {
...item,
id,
};
return itemSpecs;
}
return undefined;
}
generateHeldItemConfiguration(restrictedIds?: HeldItemId[]): HeldItemConfiguration {
const config: HeldItemConfiguration = [];
for (const [k, item] of Object.entries(this.heldItems)) {
const id = Number(k);
if (item && (!restrictedIds || id in restrictedIds)) {
const specs: HeldItemSpecs = { ...item, id };
config.push({ entry: specs, count: 1 });
}
}
for (const [k, item] of Object.entries(this.formChangeItems)) {
const id = Number(k);
const specs: FormChangeItemSpecs = { ...item, id };
config.push({ entry: specs, count: 1 });
}
return config;
}
generateSaveData(): HeldItemSaveData {
const saveData: HeldItemSaveData = [];
for (const [k, item] of Object.entries(this.heldItems)) {
const id = Number(k);
if (item) {
const specs: HeldItemSpecs = { ...item, id };
saveData.push(specs);
}
}
for (const [k, item] of Object.entries(this.formChangeItems)) {
const id = Number(k);
const specs: FormChangeItemSpecs = { ...item, id };
saveData.push(specs);
}
return saveData;
}
getHeldItems(): number[] {
return Object.keys(this.heldItems).map(k => Number(k));
}
getTransferableHeldItems(): number[] {
return Object.keys(this.heldItems)
.filter(k => allHeldItems[k].isTransferable)
.map(k => Number(k));
}
getStealableHeldItems(): number[] {
return Object.keys(this.heldItems)
.filter(k => allHeldItems[k].isStealable)
.map(k => Number(k));
}
getSuppressableHeldItems(): number[] {
return Object.keys(this.heldItems)
.filter(k => allHeldItems[k].isSuppressable)
.map(k => Number(k));
}
hasItem(itemType: HeldItemId): boolean {
return itemType in this.heldItems;
}
hasItemCategory(categoryId: HeldItemCategoryId): boolean {
return Object.keys(this.heldItems).some(id => isItemInCategory(Number(id), categoryId));
}
getStack(itemType: HeldItemId): number {
const item = this.heldItems[itemType];
return item ? item.stack : 0;
}
// Use for tests if necessary to go over stack limit
setStack(itemType: HeldItemId, stack: number): void {
const item = this.heldItems[itemType];
if (item) {
item.stack = stack;
}
}
isMaxStack(itemType: HeldItemId): boolean {
const item = this.heldItems[itemType];
return item ? item.stack >= allHeldItems[itemType].getMaxStackCount() : false;
}
overrideItems(newItems: HeldItemDataMap) {
this.heldItems = newItems;
// The following is to allow randomly generated item configs to have stack 0
for (const [item, properties] of Object.entries(this.heldItems)) {
if (!properties || properties.stack <= 0) {
delete this.heldItems[item];
}
}
}
add(itemType: HeldItemId | HeldItemSpecs, addStack = 1): boolean {
if (isHeldItemSpecs(itemType)) {
return this.addItemWithSpecs(itemType);
}
const maxStack = allHeldItems[itemType].getMaxStackCount();
const item = this.heldItems[itemType];
if (item) {
// TODO: We may want an error message of some kind instead
if (item.stack < maxStack) {
item.stack = Math.min(item.stack + addStack, maxStack);
return true;
}
} else {
this.heldItems[itemType] = { stack: Math.min(addStack, maxStack) };
return true;
}
return false;
}
addItemWithSpecs(itemSpecs: HeldItemSpecs): boolean {
const id = itemSpecs.id;
const maxStack = allHeldItems[id].getMaxStackCount();
const item = this.heldItems[id];
const tempStack = item?.stack ?? 0;
this.heldItems[id] = itemSpecs;
this.heldItems[id].stack = Math.min(itemSpecs.stack + tempStack, maxStack);
return true;
}
remove(itemType: HeldItemId, removeStack = 1, all = false) {
const item = this.heldItems[itemType];
if (item) {
item.stack -= removeStack;
if (all || item.stack <= 0) {
delete this.heldItems[itemType];
}
}
}
filterRequestedItems(requestedItems: (HeldItemCategoryId | HeldItemId)[], transferableOnly = true, exclude = false) {
const currentItems = transferableOnly ? this.getTransferableHeldItems() : this.getHeldItems();
return currentItems.filter(it => !exclude && isItemInRequested(it, requestedItems));
}
getHeldItemCount(): number {
let total = 0;
for (const properties of Object.values(this.heldItems)) {
total += properties?.stack ?? 0;
}
return total;
}
addFormChangeItem(id: FormChangeItem) {
if (!(id in this.formChangeItems)) {
this.formChangeItems[id] = { active: false };
}
}
addFormChangeItemWithSpecs(item: FormChangeItemSpecs) {
if (!(item.id in this.formChangeItems)) {
this.formChangeItems[item.id] = { active: item.active };
}
}
hasFormChangeItem(id: FormChangeItem): boolean {
return id in this.formChangeItems;
}
hasActiveFormChangeItem(id: FormChangeItem): boolean {
const item = this.formChangeItems[id];
if (item) {
return item.active;
}
return false;
}
getFormChangeItems(): FormChangeItem[] {
return Object.keys(this.formChangeItems).map(k => Number(k));
}
getActiveFormChangeItems(): FormChangeItem[] {
return this.getFormChangeItems().filter(m => this.formChangeItems[m]?.active);
}
toggleActive(id: FormChangeItem) {
const item = this.formChangeItems[id];
if (item) {
item.active = !item.active;
}
}
clearItems() {
this.heldItems = {};
this.formChangeItems = {};
}
}

View File

@ -51,27 +51,6 @@ import {
BATTLE_STATS, BATTLE_STATS,
EFFECTIVE_STATS, EFFECTIVE_STATS,
} from "#enums/stat"; } from "#enums/stat";
import {
EnemyDamageBoosterModifier,
EnemyDamageReducerModifier,
EnemyFusionChanceModifier,
HiddenAbilityRateBoosterModifier,
BaseStatModifier,
PokemonFriendshipBoosterModifier,
PokemonHeldItemModifier,
PokemonNatureWeightModifier,
ShinyRateBoosterModifier,
SurviveDamageModifier,
TempStatStageBoosterModifier,
TempCritBoosterModifier,
StatBoosterModifier,
CritBoosterModifier,
PokemonBaseStatFlatModifier,
PokemonBaseStatTotalModifier,
PokemonIncrementingStatModifier,
EvoTrackerModifier,
PokemonMultiHitModifier,
} from "#app/modifier/modifier";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { Status, getRandomStatus } from "#app/data/status-effect"; import { Status, getRandomStatus } from "#app/data/status-effect";
@ -135,7 +114,7 @@ import type { TrainerSlot } from "#enums/trainer-slot";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import i18next from "i18next"; import i18next from "i18next";
import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { speciesEggMoves } from "#app/data/balance/egg-moves";
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { applyChallenges } from "#app/data/challenge"; import { applyChallenges } from "#app/data/challenge";
import { ChallengeType } from "#enums/challenge-type"; import { ChallengeType } from "#enums/challenge-type";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
@ -173,6 +152,10 @@ import { doShinySparkleAnim } from "#app/field/anims";
import { MoveFlags } from "#enums/MoveFlags"; import { MoveFlags } from "#enums/MoveFlags";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader";
import { PokemonItemManager } from "./pokemon-held-item-manager";
import { applyHeldItems } from "#app/items/all-held-items";
import { HELD_ITEM_EFFECT } from "#app/items/held-item";
import { HeldItemId } from "#enums/held-item-id";
import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode"; import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
@ -182,6 +165,9 @@ import type { IllusionData } from "#app/@types/illusion-data";
import type { TurnMove } from "#app/@types/turn-move"; import type { TurnMove } from "#app/@types/turn-move";
import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result"; import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result";
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types";
import { TRAINER_ITEM_EFFECT } from "#app/items/trainer-item";
import type { HeldItemConfiguration } from "#app/items/held-item-data-types";
import { assignItemsFromConfiguration } from "#app/items/held-item-pool";
import { getTerrainBlockMessage } from "#app/data/terrain"; import { getTerrainBlockMessage } from "#app/data/terrain";
import { LearnMoveSituation } from "#enums/learn-move-situation"; import { LearnMoveSituation } from "#enums/learn-move-situation";
@ -297,6 +283,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
private shinySparkle: Phaser.GameObjects.Sprite; private shinySparkle: Phaser.GameObjects.Sprite;
public heldItemManager: PokemonItemManager;
// TODO: Rework this eventually // TODO: Rework this eventually
constructor( constructor(
x: number, x: number,
@ -310,6 +298,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
variant?: Variant, variant?: Variant,
ivs?: number[], ivs?: number[],
nature?: Nature, nature?: Nature,
heldItemConfig?: HeldItemConfiguration,
dataSource?: Pokemon | PokemonData, dataSource?: Pokemon | PokemonData,
) { ) {
super(globalScene, x, y); super(globalScene, x, y);
@ -339,6 +328,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate); this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate);
this.levelExp = dataSource?.levelExp || 0; this.levelExp = dataSource?.levelExp || 0;
this.heldItemManager = new PokemonItemManager();
if (heldItemConfig) {
assignItemsFromConfiguration(heldItemConfig, this);
}
if (dataSource) { if (dataSource) {
this.id = dataSource.id; this.id = dataSource.id;
this.hp = dataSource.hp; this.hp = dataSource.hp;
@ -416,7 +410,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (level > 1) { if (level > 1) {
const fused = new BooleanHolder(globalScene.gameMode.isSplicedOnly); const fused = new BooleanHolder(globalScene.gameMode.isSplicedOnly);
if (!fused.value && this.isEnemy() && !this.hasTrainer()) { if (!fused.value && this.isEnemy() && !this.hasTrainer()) {
globalScene.applyModifier(EnemyFusionChanceModifier, false, fused); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_FUSED_CHANCE, { booleanHolder: fused });
} }
if (fused.value) { if (fused.value) {
@ -599,7 +593,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// Roll for hidden ability chance, applying any ability charms for enemy mons // Roll for hidden ability chance, applying any ability charms for enemy mons
const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE);
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
} }
// If the roll succeeded and we have one, use HA; otherwise pick a random ability // If the roll succeeded and we have one, use HA; otherwise pick a random ability
@ -1119,14 +1115,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.setScale(this.getSpriteScale()); this.setScale(this.getSpriteScale());
} }
getHeldItems(): PokemonHeldItemModifier[] { getHeldItems(): HeldItemId[] {
if (!globalScene) { return this.heldItemManager.getHeldItems();
return [];
}
return globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id,
this.isPlayer(),
) as PokemonHeldItemModifier[];
} }
updateScale(): void { updateScale(): void {
@ -1356,8 +1346,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
getCritStage(source: Pokemon, move: Move): number { getCritStage(source: Pokemon, move: Move): number {
const critStage = new NumberHolder(0); const critStage = new NumberHolder(0);
applyMoveAttrs("HighCritAttr", source, this, move, critStage); applyMoveAttrs("HighCritAttr", source, this, move, critStage);
globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage); applyHeldItems(HELD_ITEM_EFFECT.CRIT_BOOST, { pokemon: source, critStage: critStage });
globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.TEMP_CRIT_BOOSTER, { numberHolder: critStage });
applyAbAttrs("BonusCritAbAttr", { pokemon: source, critStage }); applyAbAttrs("BonusCritAbAttr", { pokemon: source, critStage });
const critBoostTag = source.getTag(CritBoostTag); const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) { if (critBoostTag) {
@ -1411,7 +1401,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
): number { ): number {
const statVal = new NumberHolder(this.getStat(stat, false)); const statVal = new NumberHolder(this.getStat(stat, false));
if (!ignoreHeldItems) { if (!ignoreHeldItems) {
globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statVal); applyHeldItems(HELD_ITEM_EFFECT.STAT_BOOST, { pokemon: this, stat: stat, statValue: statVal });
} }
// The Ruin abilities here are never ignored, but they reveal themselves on summon anyway // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway
@ -1519,7 +1509,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const statHolder = new NumberHolder(Math.floor((2 * baseStats[s] + this.ivs[s]) * this.level * 0.01)); const statHolder = new NumberHolder(Math.floor((2 * baseStats[s] + this.ivs[s]) * this.level * 0.01));
if (s === Stat.HP) { if (s === Stat.HP) {
statHolder.value = statHolder.value + this.level + 10; statHolder.value = statHolder.value + this.level + 10;
globalScene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, s, statHolder); applyHeldItems(HELD_ITEM_EFFECT.INCREMENTING_STAT, { pokemon: this, stat: s, statHolder: statHolder });
if (this.hasAbility(AbilityId.WONDER_GUARD, false, true)) { if (this.hasAbility(AbilityId.WONDER_GUARD, false, true)) {
statHolder.value = 1; statHolder.value = 1;
} }
@ -1534,14 +1524,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} else { } else {
statHolder.value += 5; statHolder.value += 5;
const natureStatMultiplier = new NumberHolder(getNatureStatMultiplier(this.getNature(), s)); const natureStatMultiplier = new NumberHolder(getNatureStatMultiplier(this.getNature(), s));
globalScene.applyModifier(PokemonNatureWeightModifier, this.isPlayer(), this, natureStatMultiplier); applyHeldItems(HELD_ITEM_EFFECT.NATURE_WEIGHT_BOOSTER, { pokemon: this, multiplier: natureStatMultiplier });
if (natureStatMultiplier.value !== 1) { if (natureStatMultiplier.value !== 1) {
statHolder.value = Math.max( statHolder.value = Math.max(
Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](statHolder.value * natureStatMultiplier.value), Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](statHolder.value * natureStatMultiplier.value),
1, 1,
); );
} }
globalScene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, s, statHolder); applyHeldItems(HELD_ITEM_EFFECT.INCREMENTING_STAT, { pokemon: this, stat: s, statHolder: statHolder });
} }
statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, Number.MAX_SAFE_INTEGER); statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, Number.MAX_SAFE_INTEGER);
@ -1554,9 +1544,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const baseStats = this.getSpeciesForm(true).baseStats.slice(0); const baseStats = this.getSpeciesForm(true).baseStats.slice(0);
applyChallenges(ChallengeType.FLIP_STAT, this, baseStats); applyChallenges(ChallengeType.FLIP_STAT, this, baseStats);
// Shuckle Juice // Shuckle Juice
globalScene.applyModifiers(PokemonBaseStatTotalModifier, this.isPlayer(), this, baseStats); applyHeldItems(HELD_ITEM_EFFECT.BASE_STAT_TOTAL, { pokemon: this, baseStats: baseStats });
// Old Gateau // Old Gateau
globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); applyHeldItems(HELD_ITEM_EFFECT.BASE_STAT_FLAT, { pokemon: this, baseStats: baseStats });
if (this.isFusion()) { if (this.isFusion()) {
const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats;
applyChallenges(ChallengeType.FLIP_STAT, this, fusionBaseStats); applyChallenges(ChallengeType.FLIP_STAT, this, fusionBaseStats);
@ -1570,7 +1560,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
// Vitamins // Vitamins
globalScene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); applyHeldItems(HELD_ITEM_EFFECT.BASE_STAT_BOOSTER, { pokemon: this, baseStats: baseStats });
return baseStats; return baseStats;
} }
@ -2180,8 +2170,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first * Gets the weight of the Pokemon with subtractive abilities (Autotomize) happening first
* and then multiplicative modifiers happening after (Heavy Metal and Light Metal) * and then multiplicative abilities happening after (Heavy Metal and Light Metal)
* @returns the kg of the Pokemon (minimum of 0.1) * @returns the kg of the Pokemon (minimum of 0.1)
*/ */
public getWeight(): number { public getWeight(): number {
@ -2794,7 +2784,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold });
} }
} else { } else {
shinyThreshold.value = thresholdOverride; shinyThreshold.value = thresholdOverride;
@ -2826,7 +2816,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (timedEventManager.isEventActive()) { if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier(); shinyThreshold.value *= timedEventManager.getShinyMultiplier();
} }
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold });
} }
this.shiny = randSeedInt(65536) < shinyThreshold.value; this.shiny = randSeedInt(65536) < shinyThreshold.value;
@ -2898,7 +2888,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const haThreshold = new NumberHolder(thresholdOverride ?? BASE_HIDDEN_ABILITY_CHANCE); const haThreshold = new NumberHolder(thresholdOverride ?? BASE_HIDDEN_ABILITY_CHANCE);
if (applyModifiersToOverride) { if (applyModifiersToOverride) {
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, haThreshold); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, { numberHolder: haThreshold });
} }
} }
@ -2912,7 +2902,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public generateFusionSpecies(forStarter?: boolean): void { public generateFusionSpecies(forStarter?: boolean): void {
const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE);
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
} }
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
@ -3040,11 +3032,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) { if (tmPoolTiers[moveId] === RewardTier.COMMON && this.level >= 15) {
movePool.push([moveId, 4]); movePool.push([moveId, 4]);
} else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) { } else if (tmPoolTiers[moveId] === RewardTier.GREAT && this.level >= 30) {
movePool.push([moveId, 8]); movePool.push([moveId, 8]);
} else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) { } else if (tmPoolTiers[moveId] === RewardTier.ULTRA && this.level >= 50) {
movePool.push([moveId, 14]); movePool.push([moveId, 14]);
} }
} }
@ -3426,7 +3418,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!ignoreStatStage.value) { if (!ignoreStatStage.value) {
const statStageMultiplier = new NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value)); const statStageMultiplier = new NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value));
if (!ignoreHeldItems) { if (!ignoreHeldItems) {
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.TEMP_STAT_STAGE_BOOSTER, {
numberHolder: statStageMultiplier,
});
} }
return Math.min(statStageMultiplier.value, 4); return Math.min(statStageMultiplier.value, 4);
} }
@ -3460,7 +3454,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: this, stat: Stat.EVA, ignored: ignoreEvaStatStage }); applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: this, stat: Stat.EVA, ignored: ignoreEvaStatStage });
applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage); applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage);
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.TEMP_ACCURACY_BOOSTER, { numberHolder: userAccStage });
userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6); userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6);
targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value; targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value;
@ -3713,14 +3707,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyMoveAttrs("FixedDamageAttr", source, this, move, fixedDamage); applyMoveAttrs("FixedDamageAttr", source, this, move, fixedDamage);
if (fixedDamage.value) { if (fixedDamage.value) {
const multiLensMultiplier = new NumberHolder(1); const multiLensMultiplier = new NumberHolder(1);
globalScene.applyModifiers( applyHeldItems(HELD_ITEM_EFFECT.MULTI_HIT, {
PokemonMultiHitModifier, pokemon: source,
source.isPlayer(), moveId: move.id,
source, damageMultiplier: multiLensMultiplier,
move.id, });
null,
multiLensMultiplier,
);
fixedDamage.value = toDmgValue(fixedDamage.value * multiLensMultiplier.value); fixedDamage.value = toDmgValue(fixedDamage.value * multiLensMultiplier.value);
return { return {
@ -3764,14 +3755,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** Multiplier for moves enhanced by Multi-Lens and/or Parental Bond */ /** Multiplier for moves enhanced by Multi-Lens and/or Parental Bond */
const multiStrikeEnhancementMultiplier = new NumberHolder(1); const multiStrikeEnhancementMultiplier = new NumberHolder(1);
globalScene.applyModifiers( applyHeldItems(HELD_ITEM_EFFECT.MULTI_HIT, {
PokemonMultiHitModifier, pokemon: source,
source.isPlayer(), moveId: move.id,
source, damageMultiplier: multiStrikeEnhancementMultiplier,
move.id, });
null,
multiStrikeEnhancementMultiplier,
);
if (!ignoreSourceAbility) { if (!ignoreSourceAbility) {
applyAbAttrs("AddSecondStrikeAbAttr", { applyAbAttrs("AddSecondStrikeAbAttr", {
@ -3890,10 +3878,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** Apply the enemy's Damage and Resistance tokens */ /** Apply the enemy's Damage and Resistance tokens */
if (!source.isPlayer()) { if (!source.isPlayer()) {
globalScene.applyModifiers(EnemyDamageBoosterModifier, false, damage); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_BOOSTER, { numberHolder: damage });
} }
if (!this.isPlayer()) { if (!this.isPlayer()) {
globalScene.applyModifiers(EnemyDamageReducerModifier, false, damage); globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_REDUCER, { numberHolder: damage });
} }
const abAttrParams: PreAttackModifyDamageAbAttrParams = { const abAttrParams: PreAttackModifyDamageAbAttrParams = {
@ -3903,7 +3891,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
simulated, simulated,
damage, damage,
}; };
/** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ /** Apply this Pokemon's post-calc defensive attributes (e.g. Fur Coat) */
if (!ignoreAbility) { if (!ignoreAbility) {
applyAbAttrs("ReceivedMoveDamageMultiplierAbAttr", abAttrParams); applyAbAttrs("ReceivedMoveDamageMultiplierAbAttr", abAttrParams);
@ -4005,7 +3993,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
surviveDamage.value = this.lapseTag(BattlerTagType.ENDURE_TOKEN); surviveDamage.value = this.lapseTag(BattlerTagType.ENDURE_TOKEN);
} }
if (!surviveDamage.value) { if (!surviveDamage.value) {
globalScene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage); applyHeldItems(HELD_ITEM_EFFECT.SURVIVE_CHANCE, { pokemon: this, surviveDamage: surviveDamage });
} }
if (surviveDamage.value) { if (surviveDamage.value) {
damage = this.hp - 1; damage = this.hp - 1;
@ -4447,7 +4435,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.setScale(this.getSpriteScale()); this.setScale(this.getSpriteScale());
this.loadAssets().then(() => { this.loadAssets().then(() => {
this.calculateStats(); this.calculateStats();
globalScene.updateModifiers(this.isPlayer(), true); globalScene.updateItems(this.isPlayer());
Promise.all([this.updateInfo(), globalScene.updateFieldScale()]).then(() => resolve()); Promise.all([this.updateInfo(), globalScene.updateFieldScale()]).then(() => resolve());
}); });
}); });
@ -5555,15 +5543,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param forBattle If `false`, do not trigger in-battle effects (such as Unburden) from losing the item. For example, set this to `false` if the Pokemon is giving away the held item for a Mystery Encounter. Default is `true`. * @param forBattle If `false`, do not trigger in-battle effects (such as Unburden) from losing the item. For example, set this to `false` if the Pokemon is giving away the held item for a Mystery Encounter. Default is `true`.
* @returns `true` if the item was removed successfully, `false` otherwise. * @returns `true` if the item was removed successfully, `false` otherwise.
*/ */
public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle = true): boolean { public loseHeldItem(heldItemId: HeldItemId, forBattle = true): boolean {
if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) { if (!this.heldItemManager.hasItem(heldItemId)) {
return false; return false;
} }
heldItem.stackCount--; this.heldItemManager.remove(heldItemId);
if (heldItem.stackCount <= 0) {
globalScene.removeModifier(heldItem, this.isEnemy());
}
if (forBattle) { if (forBattle) {
applyAbAttrs("PostItemLostAbAttr", { pokemon: this }); applyAbAttrs("PostItemLostAbAttr", { pokemon: this });
} }
@ -5585,13 +5571,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
this.turnData.berriesEaten.push(berryType); this.turnData.berriesEaten.push(berryType);
} }
getPersistentTreasureCount(): number {
return (
this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length +
globalScene.findModifiers(m => m.is("MoneyMultiplierModifier") || m.is("ExtraModifierModifier")).length
);
}
} }
export class PlayerPokemon extends Pokemon { export class PlayerPokemon extends Pokemon {
@ -5608,9 +5587,24 @@ export class PlayerPokemon extends Pokemon {
variant?: Variant, variant?: Variant,
ivs?: number[], ivs?: number[],
nature?: Nature, nature?: Nature,
heldItemConfig?: HeldItemConfiguration,
dataSource?: Pokemon | PokemonData, dataSource?: Pokemon | PokemonData,
) { ) {
super(106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource); super(
106,
148,
species,
level,
abilityIndex,
formIndex,
gender,
shiny,
variant,
ivs,
nature,
heldItemConfig,
dataSource,
);
if (Overrides.STATUS_OVERRIDE) { if (Overrides.STATUS_OVERRIDE) {
this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4); this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4);
@ -5754,7 +5748,7 @@ export class PlayerPokemon extends Pokemon {
fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null, fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null,
].filter(d => !!d); ].filter(d => !!d);
const amount = new NumberHolder(friendship); const amount = new NumberHolder(friendship);
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); applyHeldItems(HELD_ITEM_EFFECT.FRIENDSHIP_BOOSTER, { pokemon: this, friendship: amount });
const candyFriendshipMultiplier = globalScene.gameMode.isClassic const candyFriendshipMultiplier = globalScene.gameMode.isClassic
? timedEventManager.getClassicFriendshipMultiplier() ? timedEventManager.getClassicFriendshipMultiplier()
: 1; : 1;
@ -5814,6 +5808,7 @@ export class PlayerPokemon extends Pokemon {
this.variant, this.variant,
this.ivs, this.ivs,
this.nature, this.nature,
this.heldItemManager.generateHeldItemConfiguration(),
this, this,
); );
this.fusionSpecies = originalFusionSpecies; this.fusionSpecies = originalFusionSpecies;
@ -5836,6 +5831,7 @@ export class PlayerPokemon extends Pokemon {
this.variant, this.variant,
this.ivs, this.ivs,
this.nature, this.nature,
this.heldItemManager.generateHeldItemConfiguration(),
this, this,
); );
} }
@ -5908,9 +5904,9 @@ export class PlayerPokemon extends Pokemon {
}); });
}; };
if (preEvolution.speciesId === SpeciesId.GIMMIGHOUL) { if (preEvolution.speciesId === SpeciesId.GIMMIGHOUL) {
const evotracker = this.getHeldItems().filter(m => m instanceof EvoTrackerModifier)[0] ?? null; const evotracker = this.heldItemManager.hasItem(HeldItemId.GIMMIGHOUL_EVO_TRACKER);
if (evotracker) { if (evotracker) {
globalScene.removeModifier(evotracker); this.heldItemManager.remove(HeldItemId.GIMMIGHOUL_EVO_TRACKER, 0, true);
} }
} }
if (!globalScene.gameMode.isDaily || this.metBiome > -1) { if (!globalScene.gameMode.isDaily || this.metBiome > -1) {
@ -5963,16 +5959,12 @@ export class PlayerPokemon extends Pokemon {
globalScene.getPlayerParty().push(newPokemon); globalScene.getPlayerParty().push(newPokemon);
newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies); newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies);
const modifiers = globalScene.findModifiers( //TODO: This currently does not consider any values associated with the items e.g. disabled
m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, const heldItems = this.getHeldItems();
true, heldItems.forEach(item => {
) as PokemonHeldItemModifier[]; newPokemon.heldItemManager.add(item, this.heldItemManager.getStack(item));
modifiers.forEach(m => {
const clonedModifier = m.clone() as PokemonHeldItemModifier;
clonedModifier.pokemonId = newPokemon.id;
globalScene.addModifier(clonedModifier, true);
}); });
globalScene.updateModifiers(true); globalScene.updateItems(true);
} }
} }
} }
@ -5993,6 +5985,7 @@ export class PlayerPokemon extends Pokemon {
this.variant, this.variant,
this.ivs, this.ivs,
this.nature, this.nature,
this.heldItemManager.generateHeldItemConfiguration(),
this, this,
); );
ret.loadAssets().then(() => resolve(ret)); ret.loadAssets().then(() => resolve(ret));
@ -6017,7 +6010,7 @@ export class PlayerPokemon extends Pokemon {
const updateAndResolve = () => { const updateAndResolve = () => {
this.loadAssets().then(() => { this.loadAssets().then(() => {
this.calculateStats(); this.calculateStats();
globalScene.updateModifiers(true, true); globalScene.updateItems(true);
this.updateInfo(true).then(() => resolve()); this.updateInfo(true).then(() => resolve());
}); });
}; };
@ -6083,15 +6076,11 @@ export class PlayerPokemon extends Pokemon {
} }
// combine the two mons' held items // combine the two mons' held items
const fusedPartyMemberHeldModifiers = globalScene.findModifiers( const fusedPartyMemberHeldItems = pokemon.getHeldItems();
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, for (const item of fusedPartyMemberHeldItems) {
true, globalScene.tryTransferHeldItem(item, pokemon, this, false, pokemon.heldItemManager.getStack(item), true, false);
) as PokemonHeldItemModifier[];
for (const modifier of fusedPartyMemberHeldModifiers) {
globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false);
} }
globalScene.updateModifiers(true, true); globalScene.updateItems(true);
globalScene.removePartyMemberModifiers(fusedPartyMemberIndex);
globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this); const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this);
pokemon pokemon
@ -6139,6 +6128,7 @@ export class EnemyPokemon extends Pokemon {
trainerSlot: TrainerSlot, trainerSlot: TrainerSlot,
boss: boolean, boss: boolean,
shinyLock = false, shinyLock = false,
heldItemConfig?: HeldItemConfiguration,
dataSource?: PokemonData, dataSource?: PokemonData,
) { ) {
super( super(
@ -6153,6 +6143,7 @@ export class EnemyPokemon extends Pokemon {
!shinyLock && dataSource ? dataSource.variant : undefined, !shinyLock && dataSource ? dataSource.variant : undefined,
undefined, undefined,
dataSource ? dataSource.nature : undefined, dataSource ? dataSource.nature : undefined,
heldItemConfig,
dataSource, dataSource,
); );
@ -6789,6 +6780,7 @@ export class EnemyPokemon extends Pokemon {
this.variant, this.variant,
this.ivs, this.ivs,
this.nature, this.nature,
this.heldItemManager.generateHeldItemConfiguration(),
this, this,
); );

View File

@ -12,7 +12,6 @@ import { TrainerPoolTier } from "#enums/trainer-pool-tier";
import { TeraAIMode } from "#enums/tera-ai-mode"; import { TeraAIMode } from "#enums/tera-ai-mode";
import type { EnemyPokemon } from "#app/field/pokemon"; import type { EnemyPokemon } from "#app/field/pokemon";
import { randSeedWeightedItem, randSeedItem, randSeedInt } from "#app/utils/common"; import { randSeedWeightedItem, randSeedItem, randSeedInt } from "#app/utils/common";
import type { PersistentModifier } from "#app/modifier/modifier";
import { ArenaTrapTag } from "#app/data/arena-tag"; import { ArenaTrapTag } from "#app/data/arena-tag";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { getIsInitialized, initI18n } from "#app/plugins/i18n"; import { getIsInitialized, initI18n } from "#app/plugins/i18n";
@ -22,6 +21,7 @@ import { SpeciesId } from "#enums/species-id";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { signatureSpecies } from "#app/data/balance/signature-species"; import { signatureSpecies } from "#app/data/balance/signature-species";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import type { TrainerItemConfiguration } from "#app/items/trainer-item-data-types";
export default class Trainer extends Phaser.GameObjects.Container { export default class Trainer extends Phaser.GameObjects.Container {
public config: TrainerConfig; public config: TrainerConfig;
@ -647,7 +647,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
} }
} }
genModifiers(party: EnemyPokemon[]): PersistentModifier[] { genTrainerItems(party: EnemyPokemon[]): TrainerItemConfiguration {
if (this.config.genModifiersFunc) { if (this.config.genModifiersFunc) {
return this.config.genModifiersFunc(party); return this.config.genModifiersFunc(party);
} }

215
src/items/all-held-items.ts Normal file
View File

@ -0,0 +1,215 @@
import { allHeldItems } from "#app/data/data-lists";
import { getEnumValues } from "#app/utils/common";
import { BerryType } from "#enums/berry-type";
import { HeldItemId } from "#enums/held-item-id";
import type { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id";
import { Stat, type PermanentStat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { HELD_ITEM_EFFECT } from "./held-item";
import { type ACCURACY_BOOST_PARAMS, AccuracyBoosterHeldItem } from "./held-items/accuracy-booster";
import {
type ATTACK_TYPE_BOOST_PARAMS,
AttackTypeBoosterHeldItem,
attackTypeToHeldItem,
} from "./held-items/attack-type-booster";
import {
type BASE_STAT_BOOSTER_PARAMS,
BaseStatBoosterHeldItem,
permanentStatToHeldItem,
} from "./held-items/base-stat-booster";
import { type BASE_STAT_FLAT_PARAMS, BaseStatFlatHeldItem } from "./held-items/base-stat-flat";
import { type BASE_STAT_TOTAL_PARAMS, BaseStatTotalHeldItem } from "./held-items/base-stat-total";
import { type BATON_PARAMS, BatonHeldItem } from "./held-items/baton";
import { type BERRY_PARAMS, BerryHeldItem, berryTypeToHeldItem } from "./held-items/berry";
import { type BYPASS_SPEED_CHANCE_PARAMS, BypassSpeedChanceHeldItem } from "./held-items/bypass-speed-chance";
import { type CRIT_BOOST_PARAMS, CritBoostHeldItem, SpeciesCritBoostHeldItem } from "./held-items/crit-booster";
import { type DAMAGE_MONEY_REWARD_PARAMS, DamageMoneyRewardHeldItem } from "./held-items/damage-money-reward";
import { type EVO_TRACKER_PARAMS, GimmighoulEvoTrackerHeldItem } from "./held-items/evo-tracker";
import { type EXP_BOOST_PARAMS, ExpBoosterHeldItem } from "./held-items/exp-booster";
import { type FIELD_EFFECT_PARAMS, FieldEffectHeldItem } from "./held-items/field-effect";
import { type FLINCH_CHANCE_PARAMS, FlinchChanceHeldItem } from "./held-items/flinch-chance";
import { type FRIENDSHIP_BOOST_PARAMS, FriendshipBoosterHeldItem } from "./held-items/friendship-booster";
import { type HIT_HEAL_PARAMS, HitHealHeldItem } from "./held-items/hit-heal";
import { type INCREMENTING_STAT_PARAMS, IncrementingStatHeldItem } from "./held-items/incrementing-stat";
import { InstantReviveHeldItem, type INSTANT_REVIVE_PARAMS } from "./held-items/instant-revive";
import {
ContactItemStealChanceHeldItem,
type ITEM_STEAL_PARAMS,
TurnEndItemStealHeldItem,
} from "./held-items/item-steal";
import { type MULTI_HIT_PARAMS, MultiHitHeldItem } from "./held-items/multi-hit";
import { type NATURE_WEIGHT_BOOST_PARAMS, NatureWeightBoosterHeldItem } from "./held-items/nature-weight-booster";
import {
ResetNegativeStatStageHeldItem,
type RESET_NEGATIVE_STAT_STAGE_PARAMS,
} from "./held-items/reset-negative-stat-stage";
import {
EvolutionStatBoostHeldItem,
SpeciesStatBoostHeldItem,
type STAT_BOOST_PARAMS,
} from "./held-items/stat-booster";
import { type SURVIVE_CHANCE_PARAMS, SurviveChanceHeldItem } from "./held-items/survive-chance";
import { type TURN_END_HEAL_PARAMS, TurnEndHealHeldItem } from "./held-items/turn-end-heal";
import { type TURN_END_STATUS_PARAMS, TurnEndStatusHeldItem } from "./held-items/turn-end-status";
export function initHeldItems() {
for (const berry of getEnumValues(BerryType)) {
let maxStackCount: number;
if ([BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(berry)) {
maxStackCount = 2;
} else {
maxStackCount = 3;
}
const berryId = berryTypeToHeldItem[berry];
allHeldItems[berryId] = new BerryHeldItem(berry, maxStackCount);
}
console.log(allHeldItems);
allHeldItems[HeldItemId.REVIVER_SEED] = new InstantReviveHeldItem(HeldItemId.REVIVER_SEED, 1);
allHeldItems[HeldItemId.WHITE_HERB] = new ResetNegativeStatStageHeldItem(HeldItemId.WHITE_HERB, 2);
// SILK_SCARF, BLACK_BELT, etc...
for (const [typeKey, heldItemType] of Object.entries(attackTypeToHeldItem)) {
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114957526
const pokemonType = Number(typeKey) as PokemonType;
allHeldItems[heldItemType] = new AttackTypeBoosterHeldItem(heldItemType, 99, pokemonType, 0.2);
}
// Items that boost specific stats
allHeldItems[HeldItemId.EVIOLITE] = new EvolutionStatBoostHeldItem(
HeldItemId.EVIOLITE,
1,
[Stat.DEF, Stat.SPDEF],
1.5,
);
allHeldItems[HeldItemId.LIGHT_BALL] = new SpeciesStatBoostHeldItem(
HeldItemId.LIGHT_BALL,
1,
[Stat.ATK, Stat.SPATK],
2,
[SpeciesId.PIKACHU],
);
allHeldItems[HeldItemId.THICK_CLUB] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.ATK], 2, [
SpeciesId.CUBONE,
SpeciesId.MAROWAK,
SpeciesId.ALOLA_MAROWAK,
]);
allHeldItems[HeldItemId.METAL_POWDER] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.DEF], 2, [
SpeciesId.DITTO,
]);
allHeldItems[HeldItemId.QUICK_POWDER] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPD], 2, [
SpeciesId.DITTO,
]);
allHeldItems[HeldItemId.DEEP_SEA_SCALE] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPDEF], 2, [
SpeciesId.CLAMPERL,
]);
allHeldItems[HeldItemId.DEEP_SEA_TOOTH] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPATK], 2, [
SpeciesId.CLAMPERL,
]);
// Items that boost the crit rate
allHeldItems[HeldItemId.SCOPE_LENS] = new CritBoostHeldItem(HeldItemId.SCOPE_LENS, 1, 1);
allHeldItems[HeldItemId.LEEK] = new SpeciesCritBoostHeldItem(HeldItemId.LEEK, 1, 2, [
SpeciesId.FARFETCHD,
SpeciesId.GALAR_FARFETCHD,
SpeciesId.SIRFETCHD,
]);
allHeldItems[HeldItemId.LUCKY_EGG] = new ExpBoosterHeldItem(HeldItemId.LUCKY_EGG, 99, 40);
allHeldItems[HeldItemId.GOLDEN_EGG] = new ExpBoosterHeldItem(HeldItemId.GOLDEN_EGG, 99, 100);
allHeldItems[HeldItemId.SOOTHE_BELL] = new FriendshipBoosterHeldItem(HeldItemId.SOOTHE_BELL, 3);
allHeldItems[HeldItemId.LEFTOVERS] = new TurnEndHealHeldItem(HeldItemId.LEFTOVERS, 4);
allHeldItems[HeldItemId.SHELL_BELL] = new HitHealHeldItem(HeldItemId.SHELL_BELL, 4);
allHeldItems[HeldItemId.FOCUS_BAND] = new SurviveChanceHeldItem(HeldItemId.FOCUS_BAND, 5);
allHeldItems[HeldItemId.QUICK_CLAW] = new BypassSpeedChanceHeldItem(HeldItemId.QUICK_CLAW, 3);
allHeldItems[HeldItemId.KINGS_ROCK] = new FlinchChanceHeldItem(HeldItemId.KINGS_ROCK, 3, 10);
allHeldItems[HeldItemId.MYSTICAL_ROCK] = new FieldEffectHeldItem(HeldItemId.MYSTICAL_ROCK, 2);
allHeldItems[HeldItemId.SOUL_DEW] = new NatureWeightBoosterHeldItem(HeldItemId.SOUL_DEW, 10);
allHeldItems[HeldItemId.WIDE_LENS] = new AccuracyBoosterHeldItem(HeldItemId.WIDE_LENS, 3, 5);
allHeldItems[HeldItemId.MULTI_LENS] = new MultiHitHeldItem(HeldItemId.MULTI_LENS, 2);
allHeldItems[HeldItemId.GOLDEN_PUNCH] = new DamageMoneyRewardHeldItem(HeldItemId.GOLDEN_PUNCH, 5);
allHeldItems[HeldItemId.BATON] = new BatonHeldItem(HeldItemId.BATON, 1);
allHeldItems[HeldItemId.GRIP_CLAW] = new ContactItemStealChanceHeldItem(HeldItemId.GRIP_CLAW, 5, 10);
allHeldItems[HeldItemId.MINI_BLACK_HOLE] = new TurnEndItemStealHeldItem(HeldItemId.MINI_BLACK_HOLE, 1)
.unstealable()
.untransferable();
allHeldItems[HeldItemId.FLAME_ORB] = new TurnEndStatusHeldItem(HeldItemId.FLAME_ORB, 1, StatusEffect.BURN);
allHeldItems[HeldItemId.TOXIC_ORB] = new TurnEndStatusHeldItem(HeldItemId.TOXIC_ORB, 1, StatusEffect.TOXIC);
// vitamins
for (const [statKey, heldItemType] of Object.entries(permanentStatToHeldItem)) {
const stat = Number(statKey) as PermanentStat;
allHeldItems[heldItemType] = new BaseStatBoosterHeldItem(heldItemType, 30, stat)
.unstealable()
.untransferable()
.unsuppressable();
}
allHeldItems[HeldItemId.SHUCKLE_JUICE_GOOD] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE_GOOD, 1, 10)
.unstealable()
.untransferable()
.unsuppressable();
allHeldItems[HeldItemId.SHUCKLE_JUICE_BAD] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE_BAD, 1, -15)
.unstealable()
.untransferable()
.unsuppressable();
allHeldItems[HeldItemId.OLD_GATEAU] = new BaseStatFlatHeldItem(HeldItemId.OLD_GATEAU, 1)
.unstealable()
.untransferable()
.unsuppressable();
allHeldItems[HeldItemId.MACHO_BRACE] = new IncrementingStatHeldItem(HeldItemId.MACHO_BRACE, 50)
.unstealable()
.untransferable()
.unsuppressable();
allHeldItems[HeldItemId.GIMMIGHOUL_EVO_TRACKER] = new GimmighoulEvoTrackerHeldItem(
HeldItemId.GIMMIGHOUL_EVO_TRACKER,
999,
SpeciesId.GIMMIGHOUL,
10,
);
}
type APPLY_HELD_ITEMS_PARAMS = {
[HELD_ITEM_EFFECT.ATTACK_TYPE_BOOST]: ATTACK_TYPE_BOOST_PARAMS;
[HELD_ITEM_EFFECT.TURN_END_HEAL]: TURN_END_HEAL_PARAMS;
[HELD_ITEM_EFFECT.HIT_HEAL]: HIT_HEAL_PARAMS;
[HELD_ITEM_EFFECT.RESET_NEGATIVE_STAT_STAGE]: RESET_NEGATIVE_STAT_STAGE_PARAMS;
[HELD_ITEM_EFFECT.EXP_BOOSTER]: EXP_BOOST_PARAMS;
[HELD_ITEM_EFFECT.BERRY]: BERRY_PARAMS;
[HELD_ITEM_EFFECT.BASE_STAT_BOOSTER]: BASE_STAT_BOOSTER_PARAMS;
[HELD_ITEM_EFFECT.INSTANT_REVIVE]: INSTANT_REVIVE_PARAMS;
[HELD_ITEM_EFFECT.STAT_BOOST]: STAT_BOOST_PARAMS;
[HELD_ITEM_EFFECT.CRIT_BOOST]: CRIT_BOOST_PARAMS;
[HELD_ITEM_EFFECT.TURN_END_STATUS]: TURN_END_STATUS_PARAMS;
[HELD_ITEM_EFFECT.SURVIVE_CHANCE]: SURVIVE_CHANCE_PARAMS;
[HELD_ITEM_EFFECT.BYPASS_SPEED_CHANCE]: BYPASS_SPEED_CHANCE_PARAMS;
[HELD_ITEM_EFFECT.FLINCH_CHANCE]: FLINCH_CHANCE_PARAMS;
[HELD_ITEM_EFFECT.FIELD_EFFECT]: FIELD_EFFECT_PARAMS;
[HELD_ITEM_EFFECT.FRIENDSHIP_BOOSTER]: FRIENDSHIP_BOOST_PARAMS;
[HELD_ITEM_EFFECT.NATURE_WEIGHT_BOOSTER]: NATURE_WEIGHT_BOOST_PARAMS;
[HELD_ITEM_EFFECT.ACCURACY_BOOSTER]: ACCURACY_BOOST_PARAMS;
[HELD_ITEM_EFFECT.MULTI_HIT]: MULTI_HIT_PARAMS;
[HELD_ITEM_EFFECT.DAMAGE_MONEY_REWARD]: DAMAGE_MONEY_REWARD_PARAMS;
[HELD_ITEM_EFFECT.BATON]: BATON_PARAMS;
[HELD_ITEM_EFFECT.CONTACT_ITEM_STEAL_CHANCE]: ITEM_STEAL_PARAMS;
[HELD_ITEM_EFFECT.TURN_END_ITEM_STEAL]: ITEM_STEAL_PARAMS;
[HELD_ITEM_EFFECT.BASE_STAT_TOTAL]: BASE_STAT_TOTAL_PARAMS;
[HELD_ITEM_EFFECT.BASE_STAT_FLAT]: BASE_STAT_FLAT_PARAMS;
[HELD_ITEM_EFFECT.INCREMENTING_STAT]: INCREMENTING_STAT_PARAMS;
[HELD_ITEM_EFFECT.EVO_TRACKER]: EVO_TRACKER_PARAMS;
};
export function applyHeldItems<T extends HELD_ITEM_EFFECT>(effect: T, params: APPLY_HELD_ITEMS_PARAMS[T]) {
const pokemon = params.pokemon;
if (pokemon) {
for (const item of Object.keys(pokemon.heldItemManager.heldItems)) {
if (allHeldItems[item].effects.includes(effect)) {
allHeldItems[item].apply(params);
}
}
}
}

View File

@ -0,0 +1,114 @@
import { allTrainerItems } from "#app/data/data-lists";
import { Stat, type TempBattleStat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { TrainerItemId } from "#enums/trainer-item-id";
import {
CriticalCatchChanceBoosterTrainerItem,
DoubleBattleChanceBoosterTrainerItem,
EnemyAttackStatusEffectChanceTrainerItem,
EnemyDamageBoosterTrainerItem,
EnemyDamageReducerTrainerItem,
EnemyEndureChanceTrainerItem,
EnemyFusionChanceTrainerItem,
EnemyStatusEffectHealChanceTrainerItem,
EnemyTurnHealTrainerItem,
ExpBoosterTrainerItem,
ExtraRewardTrainerItem,
HealingBoosterTrainerItem,
HealShopCostTrainerItem,
HiddenAbilityChanceBoosterTrainerItem,
LevelIncrementBoosterTrainerItem,
MoneyMultiplierTrainerItem,
PreserveBerryTrainerItem,
ShinyRateBoosterTrainerItem,
TempAccuracyBoosterTrainerItem,
TempCritBoosterTrainerItem,
TempStatStageBoosterTrainerItem,
tempStatToTrainerItem,
TrainerItem,
} from "./trainer-item";
export function initTrainerItems() {
allTrainerItems[TrainerItemId.MAP] = new TrainerItem(TrainerItemId.MAP, 1);
allTrainerItems[TrainerItemId.IV_SCANNER] = new TrainerItem(TrainerItemId.IV_SCANNER, 1);
allTrainerItems[TrainerItemId.LOCK_CAPSULE] = new TrainerItem(TrainerItemId.LOCK_CAPSULE, 1);
allTrainerItems[TrainerItemId.MEGA_BRACELET] = new TrainerItem(TrainerItemId.MEGA_BRACELET, 1);
allTrainerItems[TrainerItemId.DYNAMAX_BAND] = new TrainerItem(TrainerItemId.DYNAMAX_BAND, 1);
allTrainerItems[TrainerItemId.TERA_ORB] = new TrainerItem(TrainerItemId.TERA_ORB, 1);
allTrainerItems[TrainerItemId.OVAL_CHARM] = new TrainerItem(TrainerItemId.OVAL_CHARM, 5);
allTrainerItems[TrainerItemId.EXP_SHARE] = new TrainerItem(TrainerItemId.EXP_SHARE, 5);
allTrainerItems[TrainerItemId.EXP_BALANCE] = new TrainerItem(TrainerItemId.EXP_BALANCE, 4);
allTrainerItems[TrainerItemId.CANDY_JAR] = new LevelIncrementBoosterTrainerItem(TrainerItemId.CANDY_JAR, 99);
allTrainerItems[TrainerItemId.BERRY_POUCH] = new PreserveBerryTrainerItem(TrainerItemId.BERRY_POUCH, 3);
allTrainerItems[TrainerItemId.HEALING_CHARM] = new HealingBoosterTrainerItem(TrainerItemId.HEALING_CHARM, 1.1, 5);
allTrainerItems[TrainerItemId.EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.EXP_CHARM, 25, 99);
allTrainerItems[TrainerItemId.SUPER_EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.SUPER_EXP_CHARM, 60, 30);
allTrainerItems[TrainerItemId.GOLDEN_EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.GOLDEN_EXP_CHARM, 100, 10);
allTrainerItems[TrainerItemId.AMULET_COIN] = new MoneyMultiplierTrainerItem(TrainerItemId.AMULET_COIN, 5);
allTrainerItems[TrainerItemId.ABILITY_CHARM] = new HiddenAbilityChanceBoosterTrainerItem(
TrainerItemId.ABILITY_CHARM,
4,
);
allTrainerItems[TrainerItemId.GOLDEN_POKEBALL] = new ExtraRewardTrainerItem(TrainerItemId.GOLDEN_POKEBALL, 3);
allTrainerItems[TrainerItemId.SHINY_CHARM] = new ShinyRateBoosterTrainerItem(TrainerItemId.SHINY_CHARM, 4);
allTrainerItems[TrainerItemId.CATCHING_CHARM] = new CriticalCatchChanceBoosterTrainerItem(
TrainerItemId.CATCHING_CHARM,
3,
);
allTrainerItems[TrainerItemId.BLACK_SLUDGE] = new HealShopCostTrainerItem(TrainerItemId.BLACK_SLUDGE, 2.5, 1);
allTrainerItems[TrainerItemId.GOLDEN_BUG_NET] = new TrainerItem(TrainerItemId.GOLDEN_BUG_NET, 1);
allTrainerItems[TrainerItemId.LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.LURE, 10);
allTrainerItems[TrainerItemId.SUPER_LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.SUPER_LURE, 15);
allTrainerItems[TrainerItemId.MAX_LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.MAX_LURE, 30);
for (const [statKey, trainerItemType] of Object.entries(tempStatToTrainerItem)) {
const stat = Number(statKey) as TempBattleStat;
if (stat === Stat.ACC) {
allTrainerItems[trainerItemType] = new TempAccuracyBoosterTrainerItem(trainerItemType, 5);
} else {
allTrainerItems[trainerItemType] = new TempStatStageBoosterTrainerItem(trainerItemType, stat, 5);
}
}
allTrainerItems[TrainerItemId.DIRE_HIT] = new TempCritBoosterTrainerItem(TrainerItemId.DIRE_HIT, 5);
allTrainerItems[TrainerItemId.ENEMY_DAMAGE_BOOSTER] = new EnemyDamageBoosterTrainerItem(
TrainerItemId.ENEMY_DAMAGE_BOOSTER,
);
allTrainerItems[TrainerItemId.ENEMY_DAMAGE_REDUCTION] = new EnemyDamageReducerTrainerItem(
TrainerItemId.ENEMY_DAMAGE_REDUCTION,
);
allTrainerItems[TrainerItemId.ENEMY_HEAL] = new EnemyTurnHealTrainerItem(TrainerItemId.ENEMY_HEAL, 10);
allTrainerItems[TrainerItemId.ENEMY_ATTACK_POISON_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem(
TrainerItemId.ENEMY_ATTACK_POISON_CHANCE,
StatusEffect.POISON,
10,
);
allTrainerItems[TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem(
TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE,
StatusEffect.PARALYSIS,
10,
);
allTrainerItems[TrainerItemId.ENEMY_ATTACK_BURN_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem(
TrainerItemId.ENEMY_ATTACK_BURN_CHANCE,
StatusEffect.BURN,
10,
);
allTrainerItems[TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE] = new EnemyStatusEffectHealChanceTrainerItem(
TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE,
10,
);
allTrainerItems[TrainerItemId.ENEMY_ENDURE_CHANCE] = new EnemyEndureChanceTrainerItem(
TrainerItemId.ENEMY_ENDURE_CHANCE,
10,
);
allTrainerItems[TrainerItemId.ENEMY_FUSED_CHANCE] = new EnemyFusionChanceTrainerItem(
TrainerItemId.ENEMY_FUSED_CHANCE,
10,
);
}

View File

@ -0,0 +1,47 @@
import { allTrainerItems } from "./all-trainer-items";
import {
type BOOLEAN_HOLDER_PARAMS,
type NUMBER_HOLDER_PARAMS,
type POKEMON_PARAMS,
type PRESERVE_BERRY_PARAMS,
TRAINER_ITEM_EFFECT,
} from "./trainer-item";
import type { TrainerItemManager } from "./trainer-item-manager";
export type APPLY_TRAINER_ITEMS_PARAMS = {
[TRAINER_ITEM_EFFECT.LEVEL_INCREMENT_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.PRESERVE_BERRY]: PRESERVE_BERRY_PARAMS;
[TRAINER_ITEM_EFFECT.HEALING_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.EXP_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.CRITICAL_CATCH_CHANCE_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.EXTRA_REWARD]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.HEAL_SHOP_COST]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.DOUBLE_BATTLE_CHANCE_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.TEMP_STAT_STAGE_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.TEMP_ACCURACY_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.TEMP_CRIT_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_BOOSTER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_REDUCER]: NUMBER_HOLDER_PARAMS;
[TRAINER_ITEM_EFFECT.ENEMY_HEAL]: POKEMON_PARAMS;
[TRAINER_ITEM_EFFECT.ENEMY_ATTACK_STATUS_CHANCE]: POKEMON_PARAMS;
[TRAINER_ITEM_EFFECT.ENEMY_STATUS_HEAL_CHANCE]: POKEMON_PARAMS;
[TRAINER_ITEM_EFFECT.ENEMY_ENDURE_CHANCE]: POKEMON_PARAMS;
[TRAINER_ITEM_EFFECT.ENEMY_FUSED_CHANCE]: BOOLEAN_HOLDER_PARAMS;
};
export function applyTrainerItems<T extends TRAINER_ITEM_EFFECT>(
effect: T,
manager: TrainerItemManager,
params: APPLY_TRAINER_ITEMS_PARAMS[T],
) {
if (manager) {
for (const item of Object.keys(manager.trainerItems)) {
if (allTrainerItems[item].effects.includes(effect)) {
allTrainerItems[item].apply(manager, params);
}
}
}
}

View File

@ -0,0 +1,84 @@
// TODO: move to `src/@types/`
import type Pokemon from "#app/field/pokemon";
import type { FormChangeItem } from "#enums/form-change-item";
import type { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import type { RewardTier } from "#enums/reward-tier";
export type HeldItemData = {
stack: number;
disabled?: boolean;
cooldown?: number;
};
export type HeldItemDataMap = {
[key in HeldItemId]?: HeldItemData;
};
export type HeldItemSpecs = HeldItemData & {
id: HeldItemId;
};
export function isHeldItemSpecs(entry: any): entry is HeldItemSpecs {
return typeof entry.id === "number" && "stack" in entry;
}
// Types used for form change items
interface FormChangeItemData {
active: boolean;
}
export type FormChangeItemPropertyMap = {
[key in FormChangeItem]?: FormChangeItemData;
};
export type FormChangeItemSpecs = FormChangeItemData & {
id: FormChangeItem;
};
export function isFormChangeItemSpecs(entry: any): entry is FormChangeItemSpecs {
return typeof entry.id === "number" && "active" in entry;
}
export type HeldItemWeights = {
[key in HeldItemId]?: number;
};
export type HeldItemWeightFunc = (party: Pokemon[]) => number;
export type HeldItemCategoryEntry = HeldItemData & {
id: HeldItemCategoryId;
customWeights?: HeldItemWeights;
};
export function isHeldItemCategoryEntry(entry: any): entry is HeldItemCategoryEntry {
return entry?.id && isHeldItemCategoryEntry(entry.id) && "customWeights" in entry;
}
type HeldItemPoolEntry = {
entry: HeldItemId | HeldItemCategoryId | HeldItemCategoryEntry | HeldItemSpecs;
weight: number | HeldItemWeightFunc;
};
export type HeldItemPool = HeldItemPoolEntry[];
export function isHeldItemPool(value: any): value is HeldItemPool {
return Array.isArray(value) && value.every(entry => "entry" in entry && "weight" in entry);
}
export type HeldItemTieredPool = {
[key in RewardTier]?: HeldItemPool;
};
type HeldItemConfigurationEntry = {
entry: HeldItemId | HeldItemCategoryId | HeldItemCategoryEntry | HeldItemSpecs | HeldItemPool | FormChangeItemSpecs;
count?: number | (() => number);
};
export type HeldItemConfiguration = HeldItemConfigurationEntry[];
export type PokemonItemMap = {
item: HeldItemSpecs | FormChangeItemSpecs;
pokemonId: number;
};
export type HeldItemSaveData = (HeldItemSpecs | FormChangeItemSpecs)[];

324
src/items/held-item-pool.ts Normal file
View File

@ -0,0 +1,324 @@
import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { coerceArray, getEnumValues, isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#app/utils/common";
import { BerryType } from "#enums/berry-type";
import { HeldItemCategoryId, HeldItemId, HeldItemNames, isCategoryId } from "#enums/held-item-id";
import { HeldItemPoolType } from "#enums/modifier-pool-type";
import type { PokemonType } from "#enums/pokemon-type";
import { RewardTier } from "#enums/reward-tier";
import { PERMANENT_STATS } from "#enums/stat";
import { allHeldItems } from "./all-held-items";
import {
type HeldItemConfiguration,
type HeldItemPool,
type HeldItemSaveData,
type HeldItemSpecs,
type HeldItemTieredPool,
type HeldItemWeights,
isFormChangeItemSpecs,
isHeldItemCategoryEntry,
isHeldItemPool,
isHeldItemSpecs,
} from "./held-item-data-types";
import { attackTypeToHeldItem } from "./held-items/attack-type-booster";
import { permanentStatToHeldItem } from "./held-items/base-stat-booster";
import { berryTypeToHeldItem } from "./held-items/berry";
export const wildHeldItemPool: HeldItemTieredPool = {};
export const trainerHeldItemPool: HeldItemTieredPool = {};
export const dailyStarterHeldItemPool: HeldItemTieredPool = {};
export function assignDailyRunStarterHeldItems(party: PlayerPokemon[]) {
for (const p of party) {
for (let m = 0; m < 3; m++) {
const tierValue = randSeedInt(64);
const tier = getDailyRewardTier(tierValue);
const item = getNewHeldItemFromPool(
getHeldItemPool(HeldItemPoolType.DAILY_STARTER)[tier] as HeldItemPool,
p,
party,
);
p.heldItemManager.add(item);
}
}
}
function getDailyRewardTier(tierValue: number): RewardTier {
if (tierValue > 25) {
return RewardTier.COMMON;
}
if (tierValue > 12) {
return RewardTier.GREAT;
}
if (tierValue > 4) {
return RewardTier.ULTRA;
}
if (tierValue > 0) {
return RewardTier.ROGUE;
}
return RewardTier.MASTER;
}
function getHeldItemPool(poolType: HeldItemPoolType): HeldItemTieredPool {
let pool: HeldItemTieredPool;
switch (poolType) {
case HeldItemPoolType.WILD:
pool = wildHeldItemPool;
break;
case HeldItemPoolType.TRAINER:
pool = trainerHeldItemPool;
break;
case HeldItemPoolType.DAILY_STARTER:
pool = dailyStarterHeldItemPool;
break;
}
return pool;
}
// TODO: Add proper documentation to this function (once it fully works...)
export function assignEnemyHeldItemsForWave(
waveIndex: number,
count: number,
enemy: EnemyPokemon,
poolType: HeldItemPoolType.WILD | HeldItemPoolType.TRAINER,
upgradeChance = 0,
): void {
for (let i = 1; i <= count; i++) {
const item = getNewHeldItemFromTieredPool(
getHeldItemPool(poolType),
enemy,
upgradeChance && !randSeedInt(upgradeChance) ? 1 : 0,
);
if (item) {
enemy.heldItemManager.add(item);
}
}
if (!(waveIndex % 1000)) {
enemy.heldItemManager.add(HeldItemId.MINI_BLACK_HOLE);
}
}
function getRandomTier(): RewardTier {
const tierValue = randSeedInt(1024);
if (tierValue > 255) {
return RewardTier.COMMON;
}
if (tierValue > 60) {
return RewardTier.GREAT;
}
if (tierValue > 12) {
return RewardTier.ULTRA;
}
if (tierValue) {
return RewardTier.ROGUE;
}
return RewardTier.MASTER;
}
function determineItemPoolTier(pool: HeldItemTieredPool, upgradeCount?: number): RewardTier {
let tier = getRandomTier();
if (!upgradeCount) {
upgradeCount = 0;
}
tier += upgradeCount;
while (tier && !pool[tier]?.length) {
tier--;
if (upgradeCount) {
upgradeCount--;
}
}
return tier;
}
function getNewHeldItemFromTieredPool(
pool: HeldItemTieredPool,
pokemon: Pokemon,
upgradeCount: number,
): HeldItemId | HeldItemSpecs {
const tier = determineItemPoolTier(pool, upgradeCount);
const tierPool = pool[tier];
return getNewHeldItemFromPool(tierPool!, pokemon);
}
export function getNewVitaminHeldItem(customWeights: HeldItemWeights = {}, target?: Pokemon): HeldItemId {
const items = PERMANENT_STATS.map(s => permanentStatToHeldItem[s]);
const weights = items.map(t => (target?.heldItemManager.isMaxStack(t) ? 0 : (customWeights[t] ?? 1)));
const pickedIndex = pickWeightedIndex(weights);
return !isNullOrUndefined(pickedIndex) ? items[pickedIndex] : 0;
}
export function getNewBerryHeldItem(customWeights: HeldItemWeights = {}, target?: Pokemon): HeldItemId {
const berryTypes = getEnumValues(BerryType);
const items = berryTypes.map(b => berryTypeToHeldItem[b]);
const weights = items.map(t =>
target?.heldItemManager.isMaxStack(t)
? 0
: (customWeights[t] ??
(t === HeldItemId.SITRUS_BERRY || t === HeldItemId.LUM_BERRY || t === HeldItemId.LEPPA_BERRY))
? 2
: 1,
);
const pickedIndex = pickWeightedIndex(weights);
return !isNullOrUndefined(pickedIndex) ? items[pickedIndex] : 0;
}
export function getNewAttackTypeBoosterHeldItem(
pokemon: Pokemon | Pokemon[],
customWeights: HeldItemWeights = {},
target?: Pokemon,
): HeldItemId | null {
const party = coerceArray(pokemon);
// TODO: make this consider moves or abilities that change types
const attackMoveTypes = party.flatMap(p =>
p
.getMoveset()
.filter(m => m.getMove().is("AttackMove"))
.map(m => p.getMoveType(m.getMove(), true)),
);
if (!attackMoveTypes.length) {
return null;
}
const attackMoveTypeWeights = attackMoveTypes.reduce((map, type) => {
const current = map.get(type) ?? 0;
if (current < 3) {
map.set(type, current + 1);
}
return map;
}, new Map<PokemonType, number>());
const types = Array.from(attackMoveTypeWeights.keys());
const weights = types.map(type =>
target?.heldItemManager.isMaxStack(attackTypeToHeldItem[type])
? 0
: (customWeights[attackTypeToHeldItem[type]] ?? attackMoveTypeWeights.get(type)!),
);
const pickedIndex = pickWeightedIndex(weights);
return !isNullOrUndefined(pickedIndex) ? attackTypeToHeldItem[types[pickedIndex]] : 0;
}
export function getNewHeldItemFromCategory(
id: HeldItemCategoryId,
pokemon: Pokemon | Pokemon[],
customWeights: HeldItemWeights = {},
target?: Pokemon,
): HeldItemId | null {
if (id === HeldItemCategoryId.BERRY) {
return getNewBerryHeldItem(customWeights, target);
}
if (id === HeldItemCategoryId.VITAMIN) {
return getNewVitaminHeldItem(customWeights, target);
}
if (id === HeldItemCategoryId.TYPE_ATTACK_BOOSTER) {
return getNewAttackTypeBoosterHeldItem(pokemon, customWeights, target);
}
return null;
}
function getPoolWeights(pool: HeldItemPool, pokemon: Pokemon): number[] {
return pool.map(p => {
let weight = typeof p.weight === "function" ? p.weight(coerceArray(pokemon)) : p.weight;
if (typeof p.entry === "number" && !isCategoryId(p.entry)) {
const itemId = p.entry as HeldItemId;
console.log("ITEM ID: ", itemId, HeldItemNames[itemId]);
console.log(allHeldItems[itemId]);
if (pokemon.heldItemManager.getStack(itemId) >= allHeldItems[itemId].getMaxStackCount()) {
weight = 0;
}
}
return weight;
});
}
function getNewHeldItemFromPool(pool: HeldItemPool, pokemon: Pokemon, party?: Pokemon[]): HeldItemId | HeldItemSpecs {
const weights = getPoolWeights(pool, pokemon);
const pickedIndex = pickWeightedIndex(weights);
if (isNullOrUndefined(pickedIndex)) {
return 0;
}
const entry = pool[pickedIndex].entry;
if (typeof entry === "number") {
if (isCategoryId(entry)) {
return getNewHeldItemFromCategory(entry, party ?? pokemon, {}, pokemon) as HeldItemId;
}
return entry as HeldItemId;
}
if (isHeldItemCategoryEntry(entry)) {
return getNewHeldItemFromCategory(entry.id, party ?? pokemon, entry?.customWeights, pokemon) as HeldItemId;
}
return entry as HeldItemSpecs;
}
function assignItemsFromCategory(id: HeldItemCategoryId, pokemon: Pokemon, count: number) {
for (let i = 1; i <= count; i++) {
const newItem = getNewHeldItemFromCategory(id, pokemon, {}, pokemon);
if (newItem) {
pokemon.heldItemManager.add(newItem);
}
}
}
export function assignItemsFromConfiguration(config: HeldItemConfiguration, pokemon: Pokemon) {
config.forEach(item => {
const { entry, count } = item;
const actualCount = typeof count === "function" ? count() : (count ?? 1);
if (typeof entry === "number") {
if (isCategoryId(entry)) {
assignItemsFromCategory(entry, pokemon, actualCount);
}
pokemon.heldItemManager.add(entry, actualCount);
}
if (isHeldItemSpecs(entry)) {
pokemon.heldItemManager.add(entry);
}
if (isFormChangeItemSpecs(entry)) {
pokemon.heldItemManager.addFormChangeItemWithSpecs(entry);
}
if (isHeldItemCategoryEntry(entry)) {
assignItemsFromCategory(entry.id, pokemon, actualCount);
}
if (isHeldItemPool(entry)) {
for (let i = 1; i <= actualCount; i++) {
const newItem = getNewHeldItemFromPool(entry, pokemon);
if (newItem) {
pokemon.heldItemManager.add(newItem);
}
}
}
});
}
// TODO: Handle form change items
export function saveDataToConfig(saveData: HeldItemSaveData): HeldItemConfiguration {
const config: HeldItemConfiguration = [];
for (const specs of saveData) {
config.push({ entry: specs, count: 1 });
}
return config;
}

View File

@ -0,0 +1,47 @@
import { getHeldItemCategory, HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { RewardTier } from "#enums/reward-tier";
export const heldItemTiers = {
[HeldItemCategoryId.BERRY]: RewardTier.COMMON,
[HeldItemCategoryId.BASE_STAT_BOOST]: RewardTier.GREAT,
[HeldItemId.WHITE_HERB]: RewardTier.GREAT,
[HeldItemId.METAL_POWDER]: RewardTier.GREAT,
[HeldItemId.QUICK_POWDER]: RewardTier.GREAT,
[HeldItemId.DEEP_SEA_SCALE]: RewardTier.GREAT,
[HeldItemId.DEEP_SEA_TOOTH]: RewardTier.GREAT,
[HeldItemId.SOOTHE_BELL]: RewardTier.GREAT,
[HeldItemCategoryId.TYPE_ATTACK_BOOSTER]: RewardTier.ULTRA,
[HeldItemId.REVIVER_SEED]: RewardTier.ULTRA,
[HeldItemId.LIGHT_BALL]: RewardTier.ULTRA,
[HeldItemId.EVIOLITE]: RewardTier.ULTRA,
[HeldItemId.QUICK_CLAW]: RewardTier.ULTRA,
[HeldItemId.MYSTICAL_ROCK]: RewardTier.ULTRA,
[HeldItemId.WIDE_LENS]: RewardTier.ULTRA,
[HeldItemId.GOLDEN_PUNCH]: RewardTier.ULTRA,
[HeldItemId.TOXIC_ORB]: RewardTier.ULTRA,
[HeldItemId.FLAME_ORB]: RewardTier.ULTRA,
[HeldItemId.LUCKY_EGG]: RewardTier.ULTRA,
[HeldItemId.FOCUS_BAND]: RewardTier.ROGUE,
[HeldItemId.KINGS_ROCK]: RewardTier.ROGUE,
[HeldItemId.LEFTOVERS]: RewardTier.ROGUE,
[HeldItemId.SHELL_BELL]: RewardTier.ROGUE,
[HeldItemId.GRIP_CLAW]: RewardTier.ROGUE,
[HeldItemId.SOUL_DEW]: RewardTier.ROGUE,
[HeldItemId.BATON]: RewardTier.ROGUE,
[HeldItemId.GOLDEN_EGG]: RewardTier.ULTRA,
[HeldItemId.MINI_BLACK_HOLE]: RewardTier.MASTER,
[HeldItemId.MULTI_LENS]: RewardTier.MASTER,
};
export function getHeldItemTier(item: HeldItemId): RewardTier | undefined {
let tier = heldItemTiers[item];
if (!tier) {
const category = getHeldItemCategory(item);
tier = heldItemTiers[category];
}
return tier;
}

171
src/items/held-item.ts Normal file
View File

@ -0,0 +1,171 @@
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { HeldItemNames, type HeldItemId } from "#enums/held-item-id";
import i18next from "i18next";
export const HELD_ITEM_EFFECT = {
ATTACK_TYPE_BOOST: 1,
TURN_END_HEAL: 2,
HIT_HEAL: 3,
RESET_NEGATIVE_STAT_STAGE: 4,
EXP_BOOSTER: 5,
// Should we actually distinguish different berry effects?
BERRY: 6,
BASE_STAT_BOOSTER: 7,
INSTANT_REVIVE: 8,
STAT_BOOST: 9,
CRIT_BOOST: 10,
TURN_END_STATUS: 11,
SURVIVE_CHANCE: 12,
BYPASS_SPEED_CHANCE: 13,
FLINCH_CHANCE: 14,
FIELD_EFFECT: 15,
FRIENDSHIP_BOOSTER: 16,
NATURE_WEIGHT_BOOSTER: 17,
ACCURACY_BOOSTER: 18,
MULTI_HIT: 19,
DAMAGE_MONEY_REWARD: 20,
BATON: 21,
TURN_END_ITEM_STEAL: 22,
CONTACT_ITEM_STEAL_CHANCE: 23,
EVO_TRACKER: 40,
BASE_STAT_TOTAL: 50,
BASE_STAT_FLAT: 51,
INCREMENTING_STAT: 52,
} as const;
export type HELD_ITEM_EFFECT = (typeof HELD_ITEM_EFFECT)[keyof typeof HELD_ITEM_EFFECT];
export class HeldItem {
// public pokemonId: number;
public type: HeldItemId;
public maxStackCount: number;
public isTransferable = true;
public isStealable = true;
public isSuppressable = true;
//TODO: If this is actually never changed by any subclass, perhaps it should not be here
public soundName = "se/restore";
constructor(type: HeldItemId, maxStackCount = 1) {
this.type = type;
this.maxStackCount = maxStackCount;
this.isTransferable = true;
this.isStealable = true;
this.isSuppressable = true;
}
get name(): string {
return i18next.t(`modifierType:ModifierType.${HeldItemNames[this.type]}.name`);
}
get description(): string {
return i18next.t(`modifierType:ModifierType.${HeldItemNames[this.type]}.description`);
}
get iconName(): string {
return `${HeldItemNames[this.type]?.toLowerCase()}`;
}
// TODO: Aren't these fine as just properties to set in the subclass definition?
untransferable(): HeldItem {
this.isTransferable = false;
return this;
}
unstealable(): HeldItem {
this.isStealable = false;
return this;
}
unsuppressable(): HeldItem {
this.isSuppressable = false;
return this;
}
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114950716
getMaxStackCount(): number {
return this.maxStackCount;
}
createSummaryIcon(pokemon?: Pokemon, overrideStackCount?: number): Phaser.GameObjects.Container {
const stackCount = overrideStackCount ?? (pokemon ? this.getStackCount(pokemon) : 0);
const container = globalScene.add.container(0, 0);
const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5);
container.add(item);
const stackText = this.getIconStackText(stackCount);
if (stackText) {
container.add(stackText);
}
container.setScale(0.5);
return container;
}
createPokemonIcon(pokemon: Pokemon): Phaser.GameObjects.Container {
const container = globalScene.add.container(0, 0);
const pokemonIcon = globalScene.addPokemonIcon(pokemon, -2, 10, 0, 0.5, undefined, true);
container.add(pokemonIcon);
container.setName(pokemon.id.toString());
const item = globalScene.add
.sprite(16, 16, "items")
.setScale(0.5)
.setOrigin(0, 0.5)
.setTexture("items", this.iconName);
container.add(item);
const stackText = this.getIconStackText(this.getStackCount(pokemon));
if (stackText) {
container.add(stackText);
}
return container;
}
getIconStackText(stackCount: number): Phaser.GameObjects.BitmapText | null {
if (this.getMaxStackCount() === 1) {
return null;
}
const text = globalScene.add.bitmapText(10, 15, "item-count", stackCount.toString(), 11);
text.letterSpacing = -0.5;
if (stackCount >= this.getMaxStackCount()) {
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114955458
text.setTint(0xf89890);
}
text.setOrigin(0);
return text;
}
getStackCount(pokemon: Pokemon): number {
const stackCount = pokemon.heldItemManager.getStack(this.type);
return stackCount;
}
getScoreMultiplier(): number {
return 1;
}
}
export class ConsumableHeldItem extends HeldItem {
// Sometimes berries are not eaten, some stuff may not proc unburden...
consume(pokemon: Pokemon, isPlayer: boolean, remove = true, unburden = true): void {
if (remove) {
pokemon.heldItemManager.remove(this.type, 1);
// TODO: Turn this into updateItemBar or something
globalScene.updateItems(isPlayer);
}
if (unburden) {
applyAbAttrs("PostItemLostAbAttr", { pokemon: pokemon });
}
}
}

View File

@ -0,0 +1,47 @@
import type Pokemon from "#app/field/pokemon";
import type { NumberHolder } from "#app/utils/common";
import type { HeldItemId } from "#enums/held-item-id";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
export interface ACCURACY_BOOST_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
moveAccuracy: NumberHolder;
}
export class AccuracyBoosterHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.ACCURACY_BOOSTER];
private accuracyAmount: number;
constructor(type: HeldItemId, maxStackCount = 1, accuracy: number) {
super(type, maxStackCount);
this.accuracyAmount = accuracy;
}
/**
* Checks if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied
* @param pokemon The {@linkcode Pokemon} to apply the move accuracy boost to
* @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost
* @returns `true` if {@linkcode PokemonMoveAccuracyBoosterModifier} should be applied
*/
// override shouldApply(pokemon?: Pokemon, moveAccuracy?: NumberHolder): boolean {
// return super.shouldApply(pokemon, moveAccuracy) && !!moveAccuracy;
// }
/**
* Applies {@linkcode PokemonMoveAccuracyBoosterModifier}
* @param _pokemon The {@linkcode Pokemon} to apply the move accuracy boost to
* @param moveAccuracy {@linkcode NumberHolder} holding the move accuracy boost
* @returns always `true`
*/
apply(params: ACCURACY_BOOST_PARAMS): boolean {
const pokemon = params.pokemon;
const moveAccuracy = params.moveAccuracy;
const stackCount = pokemon.heldItemManager.getStack(this.type);
moveAccuracy.value = moveAccuracy.value + this.accuracyAmount * stackCount;
return true;
}
}

View File

@ -0,0 +1,78 @@
import { HeldItemNames, HeldItemId } from "#enums/held-item-id";
import { PokemonType } from "#enums/pokemon-type";
import i18next from "i18next";
import type { NumberHolder } from "#app/utils/common";
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
export interface ATTACK_TYPE_BOOST_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The resolved type of the move */
moveType: PokemonType;
/** Holder for the damage value */
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2119660807
movePower: NumberHolder;
}
interface AttackTypeToHeldItemMap {
[key: number]: HeldItemId;
}
export const attackTypeToHeldItem: AttackTypeToHeldItemMap = {
[PokemonType.NORMAL]: HeldItemId.SILK_SCARF,
[PokemonType.FIGHTING]: HeldItemId.BLACK_BELT,
[PokemonType.FLYING]: HeldItemId.SHARP_BEAK,
[PokemonType.POISON]: HeldItemId.POISON_BARB,
[PokemonType.GROUND]: HeldItemId.SOFT_SAND,
[PokemonType.ROCK]: HeldItemId.HARD_STONE,
[PokemonType.BUG]: HeldItemId.SILVER_POWDER,
[PokemonType.GHOST]: HeldItemId.SPELL_TAG,
[PokemonType.STEEL]: HeldItemId.METAL_COAT,
[PokemonType.FIRE]: HeldItemId.CHARCOAL,
[PokemonType.WATER]: HeldItemId.MYSTIC_WATER,
[PokemonType.GRASS]: HeldItemId.MIRACLE_SEED,
[PokemonType.ELECTRIC]: HeldItemId.MAGNET,
[PokemonType.PSYCHIC]: HeldItemId.TWISTED_SPOON,
[PokemonType.ICE]: HeldItemId.NEVER_MELT_ICE,
[PokemonType.DRAGON]: HeldItemId.DRAGON_FANG,
[PokemonType.DARK]: HeldItemId.BLACK_GLASSES,
[PokemonType.FAIRY]: HeldItemId.FAIRY_FEATHER,
};
export class AttackTypeBoosterHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_HEAL];
public moveType: PokemonType;
public powerBoost: number;
// This constructor may need a revision
constructor(type: HeldItemId, maxStackCount = 1, moveType: PokemonType, powerBoost: number) {
super(type, maxStackCount);
this.moveType = moveType;
this.powerBoost = powerBoost;
}
get name(): string {
return i18next.t(`modifierType:AttackTypeBoosterItem.${HeldItemNames[this.type]?.toLowerCase()}`);
}
get description(): string {
return i18next.t("modifierType:ModifierType.AttackTypeBoosterModifierType.description", {
moveType: i18next.t(`pokemonInfo:Type.${PokemonType[this.moveType]}`),
});
}
get iconName(): string {
return `${HeldItemNames[this.type]?.toLowerCase()}`;
}
apply(params: ATTACK_TYPE_BOOST_PARAMS): void {
const pokemon = params.pokemon;
const moveType = params.moveType;
const movePower = params.movePower;
const stackCount = pokemon.heldItemManager.getStack(this.type);
if (moveType === this.moveType && movePower.value >= 1) {
movePower.value = Math.floor(movePower.value * (1 + stackCount * this.powerBoost));
}
}
}

View File

@ -0,0 +1,81 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItemId } from "#enums/held-item-id";
import { getStatKey, type PermanentStat, Stat } from "#enums/stat";
import i18next from "i18next";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
export interface BASE_STAT_BOOSTER_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
baseStats: number[];
}
interface PermanentStatToHeldItemMap {
[key: number]: HeldItemId;
}
export const permanentStatToHeldItem: PermanentStatToHeldItemMap = {
[Stat.HP]: HeldItemId.HP_UP,
[Stat.ATK]: HeldItemId.PROTEIN,
[Stat.DEF]: HeldItemId.IRON,
[Stat.SPATK]: HeldItemId.CALCIUM,
[Stat.SPDEF]: HeldItemId.ZINC,
[Stat.SPD]: HeldItemId.CARBOS,
};
export const statBoostItems: Record<PermanentStat, string> = {
[Stat.HP]: "hp_up",
[Stat.ATK]: "protein",
[Stat.DEF]: "iron",
[Stat.SPATK]: "calcium",
[Stat.SPDEF]: "zinc",
[Stat.SPD]: "carbos",
};
export class BaseStatBoosterHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BASE_STAT_BOOSTER];
public stat: PermanentStat;
constructor(type: HeldItemId, maxStackCount = 1, stat: PermanentStat) {
super(type, maxStackCount);
this.stat = stat;
}
get name(): string {
return i18next.t(`modifierType:BaseStatBoosterItem.${statBoostItems[this.stat]}`);
}
get description(): string {
return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", {
stat: i18next.t(getStatKey(this.stat)),
});
}
get iconName(): string {
return statBoostItems[this.stat];
}
/**
* Checks if {@linkcode BaseStatModifier} should be applied to the specified {@linkcode Pokemon}.
* @param _pokemon the {@linkcode Pokemon} to be modified
* @param baseStats the base stats of the {@linkcode Pokemon}
* @returns `true` if the {@linkcode Pokemon} should be modified
*/
// override shouldApply(_pokemon?: Pokemon, baseStats?: number[]): boolean {
// return super.shouldApply(_pokemon, baseStats) && Array.isArray(baseStats);
// }
/**
* Applies the {@linkcode BaseStatModifier} to the specified {@linkcode Pokemon}.
* @param _pokemon the {@linkcode Pokemon} to be modified
* @param baseStats the base stats of the {@linkcode Pokemon}
* @returns always `true`
*/
apply(params: BASE_STAT_BOOSTER_PARAMS): boolean {
const pokemon = params.pokemon;
const stackCount = pokemon.heldItemManager.getStack(this.type);
const baseStats = params.baseStats;
baseStats[this.stat] = Math.floor(baseStats[this.stat] * (1 + stackCount * 0.1));
return true;
}
}

View File

@ -0,0 +1,71 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
import { Stat } from "#enums/stat";
import i18next from "i18next";
export interface BASE_STAT_FLAT_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
baseStats: number[];
}
/**
* Currently used by Old Gateau item
*/
export class BaseStatFlatHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BASE_STAT_FLAT];
public isTransferable = false;
get description(): string {
return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description");
}
/**
* Checks if the {@linkcode PokemonBaseStatFlatModifier} should be applied to the {@linkcode Pokemon}.
* @param pokemon The {@linkcode Pokemon} that holds the item
* @param baseStats The base stats of the {@linkcode Pokemon}
* @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied
*/
// override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean {
// return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats);
// }
/**
* Applies the {@linkcode PokemonBaseStatFlatModifier}
* @param _pokemon The {@linkcode Pokemon} that holds the item
* @param baseStats The base stats of the {@linkcode Pokemon}
* @returns always `true`
*/
apply(params: BASE_STAT_FLAT_PARAMS): boolean {
const pokemon = params.pokemon;
const baseStats = params.baseStats;
const stats = this.getStats(pokemon);
const statModifier = 20;
// Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats
baseStats.forEach((v, i) => {
if (stats.includes(i)) {
const newVal = Math.floor(v + statModifier);
baseStats[i] = Math.min(Math.max(newVal, 1), 999999);
}
});
return true;
}
/**
* Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef
* @returns Array of 3 {@linkcode Stat}s to boost
*/
getStats(pokemon: Pokemon): Stat[] {
const stats: Stat[] = [];
const baseStats = pokemon.getSpeciesForm().baseStats.slice(0);
// HP or Speed
stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD);
// Attack or SpAtk
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
// Def or SpDef
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
return stats;
}
}

View File

@ -0,0 +1,70 @@
import type Pokemon from "#app/field/pokemon";
import i18next from "i18next";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
import type { HeldItemId } from "#enums/held-item-id";
export interface BASE_STAT_TOTAL_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
baseStats: number[];
}
/**
* Currently used by Shuckle Juice item
*/
export class BaseStatTotalHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BASE_STAT_TOTAL];
public isTransferable = false;
public statModifier: number;
constructor(type: HeldItemId, maxStackCount = 1, statModifier: number) {
super(type, maxStackCount);
this.statModifier = statModifier;
}
get name(): string {
return this.statModifier > 0
? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.name")
: i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.name");
}
// TODO: where is this description shown?
get description(): string {
return this.statModifier > 0
? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.description")
: i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.description");
}
get iconName(): string {
return this.statModifier > 0 ? "berry_juice_good" : "berry_juice_bad";
}
/**
* Checks if {@linkcode PokemonBaseStatTotalModifier} should be applied to the specified {@linkcode Pokemon}.
* @param pokemon the {@linkcode Pokemon} to be modified
* @param baseStats the base stats of the {@linkcode Pokemon}
* @returns `true` if the {@linkcode Pokemon} should be modified
*/
// override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean {
// return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats);
// }
/**
* Applies the {@linkcode PokemonBaseStatTotalModifier}
* @param _pokemon the {@linkcode Pokemon} to be modified
* @param baseStats the base stats of the {@linkcode Pokemon}
* @returns always `true`
*/
apply(params: BASE_STAT_TOTAL_PARAMS): boolean {
const baseStats = params.baseStats;
// Modifies the passed in baseStats[] array
baseStats.forEach((v, i) => {
// HP is affected by half as much as other stats
const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier);
baseStats[i] = Math.min(Math.max(newVal, 1), 999999);
});
return true;
}
}

View File

@ -0,0 +1,22 @@
import type Pokemon from "#app/field/pokemon";
import type { NumberHolder } from "#app/utils/common";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
export interface BATON_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
expAmount: NumberHolder;
}
export class BatonHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BATON];
/**
* Applies {@linkcode SwitchEffectTransferModifier}
* @returns always `true`
*/
apply(): boolean {
return true;
}
}

View File

@ -0,0 +1,96 @@
import { getBerryEffectDescription, getBerryEffectFunc, getBerryName, getBerryPredicate } from "#app/data/berry";
import { BerryUsedEvent } from "#app/events/battle-scene";
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { ConsumableHeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import { BooleanHolder } from "#app/utils/common";
import { BerryType } from "#enums/berry-type";
import { HeldItemId } from "#enums/held-item-id";
import { TRAINER_ITEM_EFFECT } from "../trainer-item";
interface BerryTypeToHeldItemMap {
[key: number]: HeldItemId;
}
export const berryTypeToHeldItem: BerryTypeToHeldItemMap = {
[BerryType.SITRUS]: HeldItemId.SITRUS_BERRY,
[BerryType.LUM]: HeldItemId.LUM_BERRY,
[BerryType.ENIGMA]: HeldItemId.ENIGMA_BERRY,
[BerryType.LIECHI]: HeldItemId.LIECHI_BERRY,
[BerryType.GANLON]: HeldItemId.GANLON_BERRY,
[BerryType.PETAYA]: HeldItemId.PETAYA_BERRY,
[BerryType.APICOT]: HeldItemId.APICOT_BERRY,
[BerryType.SALAC]: HeldItemId.SALAC_BERRY,
[BerryType.LANSAT]: HeldItemId.LANSAT_BERRY,
[BerryType.STARF]: HeldItemId.STARF_BERRY,
[BerryType.LEPPA]: HeldItemId.LEPPA_BERRY,
};
export interface BERRY_PARAMS {
/** The pokemon with the berry */
pokemon: Pokemon;
}
// TODO: Maybe split up into subclasses?
export class BerryHeldItem extends ConsumableHeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BERRY];
public berryType: BerryType;
constructor(berryType: BerryType, maxStackCount = 1) {
const type = berryTypeToHeldItem[berryType];
super(type, maxStackCount);
this.berryType = berryType;
}
get name(): string {
return getBerryName(this.berryType);
}
get description(): string {
return getBerryEffectDescription(this.berryType);
}
get iconName(): string {
return `${BerryType[this.berryType].toLowerCase()}_berry`;
}
/**
* Checks if {@linkcode BerryModifier} should be applied
* @param pokemon The {@linkcode Pokemon} that holds the berry
* @returns `true` if {@linkcode BerryModifier} should be applied
*/
shouldApply(pokemon: Pokemon): boolean {
return getBerryPredicate(this.berryType)(pokemon);
}
/**
* Applies {@linkcode BerryHeldItem}
* @param pokemon The {@linkcode Pokemon} that holds the berry
* @returns always `true`
*/
apply(params: BERRY_PARAMS): boolean {
const pokemon = params.pokemon;
if (!this.shouldApply(pokemon)) {
return false;
}
const preserve = new BooleanHolder(false);
globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.PRESERVE_BERRY, { pokemon: pokemon, doPreserve: preserve });
const consumed = !preserve.value;
// munch the berry and trigger unburden-like effects
getBerryEffectFunc(this.berryType)(pokemon);
this.consume(pokemon, pokemon.isPlayer(), consumed);
// TODO: Update this method to work with held items
// Update berry eaten trackers for Belch, Harvest, Cud Chew, etc.
// Don't recover it if we proc berry pouch (no item duplication)
pokemon.recordEatenBerry(this.berryType, consumed);
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(pokemon, this.berryType));
return true;
}
}

View File

@ -0,0 +1,62 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import type { BooleanHolder } from "#app/utils/common";
import { globalScene } from "#app/global-scene";
import i18next from "i18next";
import { getPokemonNameWithAffix } from "#app/messages";
import { Command } from "#enums/command";
export interface BYPASS_SPEED_CHANCE_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
doBypassSpeed: BooleanHolder;
}
/**
* Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a
* set {@linkcode StatusEffect} at the end of a turn.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class BypassSpeedChanceHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.BYPASS_SPEED_CHANCE];
/**
* Checks if {@linkcode BypassSpeedChanceModifier} should be applied
* @param pokemon the {@linkcode Pokemon} that holds the item
* @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed
* @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied
*/
// override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean {
// return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed;
// }
/**
* Applies {@linkcode BypassSpeedChanceModifier}
* @param pokemon the {@linkcode Pokemon} that holds the item
* @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed
* @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied
*/
apply(params: BYPASS_SPEED_CHANCE_PARAMS): boolean {
const pokemon = params.pokemon;
const doBypassSpeed = params.doBypassSpeed;
const stackCount = pokemon.heldItemManager.getStack(this.type);
if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < stackCount) {
doBypassSpeed.value = true;
const isCommandFight =
globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT;
if (isCommandFight) {
globalScene.phaseManager.queueMessage(
i18next.t("modifier:bypassSpeedChanceApply", {
pokemonName: getPokemonNameWithAffix(pokemon),
itemName: this.name,
}),
);
}
return true;
}
return false;
}
}

View File

@ -0,0 +1,86 @@
import type Pokemon from "#app/field/pokemon";
import type { NumberHolder } from "#app/utils/common";
import type { HeldItemId } from "#enums/held-item-id";
import type { SpeciesId } from "#enums/species-id";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
export interface CRIT_BOOST_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
critStage: NumberHolder;
}
/**
* Modifier used for held items that apply critical-hit stage boost(s).
* using a multiplier.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class CritBoostHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.CRIT_BOOST];
/** The amount of stages by which the held item increases the current critical-hit stage value */
protected stageIncrement: number;
constructor(type: HeldItemId, maxStackCount = 1, stageIncrement: number) {
super(type, maxStackCount);
this.stageIncrement = stageIncrement;
}
/**
* Increases the current critical-hit stage value by {@linkcode stageIncrement}.
* @param _pokemon {@linkcode Pokemon} N/A
* @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level
* @returns always `true`
*/
apply(params: CRIT_BOOST_PARAMS): boolean {
params.critStage.value += this.stageIncrement;
return true;
}
}
/**
* Modifier used for held items that apply critical-hit stage boost(s)
* if the holder is of a specific {@linkcode SpeciesId}.
* @extends CritBoosterModifier
* @see {@linkcode shouldApply}
*/
export class SpeciesCritBoostHeldItem extends CritBoostHeldItem {
/** The species that the held item's critical-hit stage boost applies to */
private species: SpeciesId[];
constructor(type: HeldItemId, maxStackCount = 1, stageIncrement: number, species: SpeciesId[]) {
super(type, maxStackCount, stageIncrement);
this.species = species;
}
/**
* Checks if the holder's {@linkcode SpeciesId} (or its fused species) is listed
* in {@linkcode species}.
* @param pokemon {@linkcode Pokemon} that holds the held item
* @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level
* @returns `true` if the critical-hit level can be incremented, false otherwise
*/
// override shouldApply(pokemon: Pokemon, critStage: NumberHolder): boolean {
// return (
// super.shouldApply(pokemon, critStage) &&
// (this.species.includes(pokemon.getSpeciesForm(true).speciesId) ||
// (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId)))
// );
// }
apply(params: CRIT_BOOST_PARAMS): boolean {
const pokemon = params.pokemon;
const fitsSpecies =
this.species.includes(pokemon.getSpeciesForm(true).speciesId) ||
(pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId));
if (fitsSpecies) {
return super.apply(params);
}
return false;
}
}

View File

@ -0,0 +1,33 @@
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { NumberHolder } from "#app/utils/common";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
import { TRAINER_ITEM_EFFECT } from "../trainer-item";
export interface DAMAGE_MONEY_REWARD_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
damage: number;
}
export class DamageMoneyRewardHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.DAMAGE_MONEY_REWARD];
/**
* Applies {@linkcode DamageMoneyRewardModifier}
* @param pokemon The {@linkcode Pokemon} attacking
* @param multiplier {@linkcode NumberHolder} holding the multiplier value
* @returns always `true`
*/
apply(params: DAMAGE_MONEY_REWARD_PARAMS): boolean {
const pokemon = params.pokemon;
const damage = params.damage;
const stackCount = pokemon.heldItemManager.getStack(this.type);
const moneyAmount = new NumberHolder(Math.floor(damage * (0.5 * stackCount)));
globalScene.applyPlayerItems(TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER, { numberHolder: moneyAmount });
globalScene.addMoney(moneyAmount.value);
return true;
}
}

View File

@ -0,0 +1,57 @@
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { HeldItemId } from "#enums/held-item-id";
import type { SpeciesId } from "#enums/species-id";
import i18next from "i18next";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
import { TrainerItemId } from "#enums/trainer-item-id";
export interface EVO_TRACKER_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
}
export class EvoTrackerHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.EVO_TRACKER];
protected species: SpeciesId;
protected required: number;
public isTransferable = false;
constructor(type: HeldItemId, maxStackCount = 1, species: SpeciesId, required: number) {
super(type, maxStackCount);
this.species = species;
this.required = required;
}
/**
* Applies the {@linkcode EvoTrackerModifier}
* @returns always `true`
*/
apply(): boolean {
return true;
}
}
export class GimmighoulEvoTrackerHeldItem extends EvoTrackerHeldItem {
get name(): string {
return i18next.t("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL.name");
}
get description(): string {
return i18next.t("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL.description");
}
get iconName(): string {
return "relic_gold";
}
getStackCount(pokemon: Pokemon): number {
const stackCount =
pokemon.heldItemManager.getStack(this.type) +
pokemon.heldItemManager.getStack(HeldItemId.GOLDEN_PUNCH) +
globalScene.trainerItems.getStack(TrainerItemId.AMULET_COIN) +
globalScene.trainerItems.getStack(TrainerItemId.GOLDEN_POKEBALL);
return stackCount;
}
}

View File

@ -0,0 +1,56 @@
import type Pokemon from "#app/field/pokemon";
import type { NumberHolder } from "#app/utils/common";
import type { HeldItemId } from "#enums/held-item-id";
import i18next from "i18next";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
export interface EXP_BOOST_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
expAmount: NumberHolder;
}
export class ExpBoosterHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.EXP_BOOSTER];
private boostPercent: number;
private boostMultiplier: number;
constructor(type: HeldItemId, maxStackCount = 1, boostPercent: number) {
super(type, maxStackCount);
this.boostPercent = boostPercent;
this.boostMultiplier = boostPercent * 0.01;
}
get description(): string {
return i18next.t("modifierType:ModifierType.PokemonExpBoosterModifierType.description", {
boostPercent: this.boostPercent,
});
}
// TODO: What do we do with this? Need to look up all the shouldApply
/**
* Checks if {@linkcode PokemonExpBoosterModifier} should be applied
* @param pokemon The {@linkcode Pokemon} to apply the exp boost to
* @param boost {@linkcode NumberHolder} holding the exp boost value
* @returns `true` if {@linkcode PokemonExpBoosterModifier} should be applied
*/
// override shouldApply(pokemon: Pokemon, boost: NumberHolder): boolean {
// return super.shouldApply(pokemon, boost) && !!boost;
// }
/**
* Applies {@linkcode PokemonExpBoosterModifier}
* @param _pokemon The {@linkcode Pokemon} to apply the exp boost to
* @param boost {@linkcode NumberHolder} holding the exp boost value
* @returns always `true`
*/
apply(params: EXP_BOOST_PARAMS): boolean {
const pokemon = params.pokemon;
const expAmount = params.expAmount;
const stackCount = pokemon.heldItemManager.getStack(this.type);
expAmount.value = Math.floor(expAmount.value * (1 + stackCount * this.boostMultiplier));
return true;
}
}

View File

@ -0,0 +1,33 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import type { NumberHolder } from "#app/utils/common";
export interface FIELD_EFFECT_PARAMS {
pokemon: Pokemon;
/** The pokemon with the item */
fieldDuration: NumberHolder;
}
/**
* Modifier used for held items, namely Mystical Rock, that extend the
* duration of weather and terrain effects.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class FieldEffectHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.FIELD_EFFECT];
/**
* Provides two more turns per stack to any weather or terrain effect caused
* by the holder.
* @param pokemon {@linkcode Pokemon} that holds the held item
* @param fieldDuration {@linkcode NumberHolder} that stores the current field effect duration
* @returns `true` if the field effect extension was applied successfully
*/
apply(params: FIELD_EFFECT_PARAMS): boolean {
const pokemon = params.pokemon;
const stackCount = pokemon.heldItemManager.getStack(this.type);
params.fieldDuration.value += 2 * stackCount;
return true;
}
}

View File

@ -0,0 +1,57 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import type { BooleanHolder } from "#app/utils/common";
import type { HeldItemId } from "#enums/held-item-id";
export interface FLINCH_CHANCE_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
flinched: BooleanHolder;
}
/**
* Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a
* set {@linkcode StatusEffect} at the end of a turn.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class FlinchChanceHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.FLINCH_CHANCE];
private chance: number;
constructor(type: HeldItemId, maxStackCount = 1, chance: number) {
super(type, maxStackCount);
this.chance = chance; // 10
}
/**
* Checks if {@linkcode FlinchChanceModifier} should be applied
* @param pokemon the {@linkcode Pokemon} that holds the item
* @param flinched {@linkcode BooleanHolder} that is `true` if the pokemon flinched
* @returns `true` if {@linkcode FlinchChanceModifier} should be applied
*/
// override shouldApply(pokemon?: Pokemon, flinched?: BooleanHolder): boolean {
// return super.shouldApply(pokemon, flinched) && !!flinched;
// }
/**
* Applies {@linkcode FlinchChanceModifier} to randomly flinch targets hit.
* @param pokemon - The {@linkcode Pokemon} that holds the item
* @param flinched - A {@linkcode BooleanHolder} holding whether the pokemon has flinched
* @returns `true` if {@linkcode FlinchChanceModifier} was applied successfully
*/
apply(params: FLINCH_CHANCE_PARAMS): boolean {
const pokemon = params.pokemon;
const flinched = params.flinched;
const stackCount = pokemon.heldItemManager.getStack(this.type);
// The check for pokemon.summonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch
// TODO: Since summonData is always defined now, we can probably remove this
if (pokemon.summonData && !flinched.value && pokemon.randBattleSeedInt(100) < stackCount * this.chance) {
flinched.value = true;
return true;
}
return false;
}
}

View File

@ -0,0 +1,34 @@
import type Pokemon from "#app/field/pokemon";
import type { NumberHolder } from "#app/utils/common";
import i18next from "i18next";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
export interface FRIENDSHIP_BOOST_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
friendship: NumberHolder;
}
export class FriendshipBoosterHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.FRIENDSHIP_BOOSTER];
get description(): string {
return i18next.t("modifierType:ModifierType.PokemonFriendshipBoosterModifierType.description");
}
/**
* Applies {@linkcode PokemonFriendshipBoosterModifier}
* @param _pokemon The {@linkcode Pokemon} to apply the friendship boost to
* @param friendship {@linkcode NumberHolder} holding the friendship boost value
* @returns always `true`
*/
apply(params: FRIENDSHIP_BOOST_PARAMS): boolean {
const pokemon = params.pokemon;
const friendship = params.friendship;
const stackCount = pokemon.heldItemManager.getStack(this.type);
friendship.value = Math.floor(friendship.value * (1 + 0.5 * stackCount));
return true;
}
}

View File

@ -0,0 +1,53 @@
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import i18next from "i18next";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { toDmgValue } from "#app/utils/common";
import { getPokemonNameWithAffix } from "#app/messages";
export interface HIT_HEAL_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
}
export class HitHealHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_HEAL];
get name(): string {
return i18next.t("modifierType:ModifierType.SHELL_BELL.name");
}
get description(): string {
return i18next.t("modifierType:ModifierType.SHELL_BELL.description");
}
get iconName(): string {
return "shell_bell";
}
/**
* Applies {@linkcode HitHealModifier}
* @param pokemon The {@linkcode Pokemon} that holds the item
* @returns `true` if the {@linkcode Pokemon} was healed
*/
apply(params: HIT_HEAL_PARAMS): boolean {
const pokemon = params.pokemon;
const stackCount = pokemon.heldItemManager.getStack(this.type);
if (pokemon.turnData.totalDamageDealt > 0 && !pokemon.isFullHp()) {
// TODO: this shouldn't be undefined AFAIK
globalScene.unshiftPhase(
new PokemonHealPhase(
pokemon.getBattlerIndex(),
toDmgValue(pokemon.turnData.totalDamageDealt / 8) * stackCount,
i18next.t("modifier:hitHealApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
typeName: this.name,
}),
true,
),
);
}
return true;
}
}

View File

@ -0,0 +1,71 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
import { Stat } from "#enums/stat";
import type { NumberHolder } from "#app/utils/common";
import i18next from "i18next";
export interface INCREMENTING_STAT_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
stat: Stat;
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2135612276
statHolder: NumberHolder;
}
/**
* Currently used by Macho Brace item
*/
export class IncrementingStatHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.INCREMENTING_STAT];
public isTransferable = false;
/**
* Checks if the {@linkcode PokemonIncrementingStatModifier} should be applied to the {@linkcode Pokemon}.
* @param pokemon The {@linkcode Pokemon} that holds the item
* @param stat The affected {@linkcode Stat}
* @param statHolder The {@linkcode NumberHolder} that holds the stat
* @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied
*/
// override shouldApply(pokemon?: Pokemon, stat?: Stat, statHolder?: NumberHolder): boolean {
// return super.shouldApply(pokemon, stat, statHolder) && !!statHolder;
// }
get name(): string {
return i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_MACHO_BRACE.name") + " (new)";
}
get description(): string {
return i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_MACHO_BRACE.description");
}
/**
* Applies the {@linkcode PokemonIncrementingStatModifier}
* @param _pokemon The {@linkcode Pokemon} that holds the item
* @param stat The affected {@linkcode Stat}
* @param statHolder The {@linkcode NumberHolder} that holds the stat
* @returns always `true`
*/
apply(params: INCREMENTING_STAT_PARAMS): boolean {
const pokemon = params.pokemon;
const stackCount = pokemon.heldItemManager.getStack(this.type);
const statHolder = params.statHolder;
// Modifies the passed in stat number holder by +2 per stack for HP, +1 per stack for other stats
// If the Macho Brace is at max stacks (50), adds additional 10% to total HP and 5% to other stats
const isHp = params.stat === Stat.HP;
if (isHp) {
statHolder.value += 2 * stackCount;
if (stackCount === this.maxStackCount) {
statHolder.value = Math.floor(statHolder.value * 1.1);
}
} else {
statHolder.value += stackCount;
if (stackCount === this.maxStackCount) {
statHolder.value = Math.floor(statHolder.value * 1.05);
}
}
return true;
}
}

View File

@ -0,0 +1,68 @@
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import i18next from "i18next";
import { ConsumableHeldItem, HELD_ITEM_EFFECT } from "../held-item";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { toDmgValue } from "#app/utils/common";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
export interface INSTANT_REVIVE_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
}
/**
* Modifier used for held items, namely White Herb, that restore adverse stat
* stages in battle.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class InstantReviveHeldItem extends ConsumableHeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.INSTANT_REVIVE];
get name(): string {
return i18next.t("modifierType:ModifierType.REVIVER_SEED.name");
}
get description(): string {
return i18next.t("modifierType:ModifierType.REVIVER_SEED.description");
}
get iconName(): string {
return "reviver_seed";
}
/**
* Goes through the holder's stat stages and, if any are negative, resets that
* stat stage back to 0.
* @param pokemon {@linkcode Pokemon} that holds the item
* @returns `true` if any stat stages were reset, false otherwise
*/
apply(params: INSTANT_REVIVE_PARAMS): boolean {
const pokemon = params.pokemon;
// Restore the Pokemon to half HP
globalScene.phaseManager.unshiftPhase(
new PokemonHealPhase(
pokemon.getBattlerIndex(),
toDmgValue(pokemon.getMaxHp() / 2),
i18next.t("modifier:pokemonInstantReviveApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
typeName: this.name,
}),
false,
false,
true,
),
);
// Remove the Pokemon's FAINT status
pokemon.resetStatus(true, false, true, false);
// Reapply Commander on the Pokemon's side of the field, if applicable
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
for (const p of field) {
applyAbAttrs("CommanderAbAttr", p, null, false);
}
return true;
}
}

View File

@ -0,0 +1,168 @@
import Pokemon from "#app/field/pokemon";
import { randSeedFloat } from "#app/utils/common";
import type { HeldItemId } from "#enums/held-item-id";
import i18next from "i18next";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
import { getPokemonNameWithAffix } from "#app/messages";
import { allHeldItems } from "../all-held-items";
import { globalScene } from "#app/global-scene";
export interface ITEM_STEAL_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The pokemon to steal from (optional) */
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2135607083
target?: Pokemon;
}
// constructor(type: HeldItemId, maxStackCount = 1, boostPercent: number) {
/**
* Abstract class for held items that steal other Pokemon's items.
* @see {@linkcode TurnEndItemStealHeldItem}
* @see {@linkcode ContactItemStealChanceHeldItem}
*/
export abstract class ItemTransferHeldItem extends HeldItem {
/**
* Steals an item, chosen randomly, from a set of target Pokemon.
* @param pokemon The {@linkcode Pokemon} holding this item
* @param target The {@linkcode Pokemon} to steal from (optional)
* @param _args N/A
* @returns `true` if an item was stolen; false otherwise.
*/
apply(params: ITEM_STEAL_PARAMS): boolean {
const opponents = this.getTargets(params);
if (!opponents.length) {
return false;
}
const pokemon = params.pokemon;
//TODO: Simplify this logic here
const targetPokemon = opponents[pokemon.randBattleSeedInt(opponents.length)];
const transferredItemCount = this.getTransferredItemCount(params);
if (!transferredItemCount) {
return false;
}
// TODO: Change this logic to use held items
const transferredModifierTypes: HeldItemId[] = [];
const heldItems = targetPokemon.heldItemManager.getTransferableHeldItems();
for (let i = 0; i < transferredItemCount; i++) {
if (!heldItems.length) {
break;
}
const randItemIndex = pokemon.randBattleSeedInt(heldItems.length);
const randItem = heldItems[randItemIndex];
// TODO: Fix this after updating the various methods in battle-scene.ts
if (globalScene.tryTransferHeldItem(randItem, targetPokemon, pokemon, false)) {
transferredModifierTypes.push(randItem);
heldItems.splice(randItemIndex, 1);
}
}
for (const mt of transferredModifierTypes) {
globalScene.phaseManager.queueMessage(this.getTransferMessage(params, mt));
}
return !!transferredModifierTypes.length;
}
abstract getTargets(params: ITEM_STEAL_PARAMS): Pokemon[];
abstract getTransferredItemCount(params: ITEM_STEAL_PARAMS): number;
abstract getTransferMessage(params: ITEM_STEAL_PARAMS, itemId: HeldItemId): string;
}
/**
* Modifier for held items that steal items from the enemy at the end of
* each turn.
* @see {@linkcode modifierTypes[MINI_BLACK_HOLE]}
*/
export class TurnEndItemStealHeldItem extends ItemTransferHeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_ITEM_STEAL];
isTransferable = true;
get description(): string {
return i18next.t("modifierType:ModifierType.TurnHeldItemTransferModifierType.description");
}
/**
* Determines the targets to transfer items from when this applies.
* @param pokemon the {@linkcode Pokemon} holding this item
* @param _args N/A
* @returns the opponents of the source {@linkcode Pokemon}
*/
getTargets(params: ITEM_STEAL_PARAMS): Pokemon[] {
return params.pokemon instanceof Pokemon ? params.pokemon.getOpponents() : [];
}
getTransferredItemCount(_params: ITEM_STEAL_PARAMS): number {
return 1;
}
getTransferMessage(params: ITEM_STEAL_PARAMS, itemId: HeldItemId): string {
return i18next.t("modifier:turnHeldItemTransferApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(params.target),
itemName: allHeldItems[itemId].name,
pokemonName: params.pokemon.getNameToRender(),
typeName: this.name,
});
}
setTransferrableFalse(): void {
this.isTransferable = false;
}
}
/**
* Modifier for held items that add a chance to steal items from the target of a
* successful attack.
* @see {@linkcode modifierTypes[GRIP_CLAW]}
* @see {@linkcode HeldItemTransferModifier}
*/
export class ContactItemStealChanceHeldItem extends ItemTransferHeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.CONTACT_ITEM_STEAL_CHANCE];
public readonly chancePercent: number;
public readonly chance: number;
constructor(type: HeldItemId, maxStackCount = 1, chancePercent: number) {
super(type, maxStackCount);
this.chancePercent = chancePercent;
this.chance = chancePercent / 100;
}
get description(): string {
return i18next.t("modifierType:ModifierType.ContactHeldItemTransferChanceModifierType.description", {
chancePercent: this.chancePercent,
});
}
/**
* Determines the target to steal items from when this applies.
* @param _holderPokemon The {@linkcode Pokemon} holding this item
* @param targetPokemon The {@linkcode Pokemon} the holder is targeting with an attack
* @returns The target {@linkcode Pokemon} as array for further use in `apply` implementations
*/
getTargets(params: ITEM_STEAL_PARAMS): Pokemon[] {
return params.target ? [params.target] : [];
}
getTransferredItemCount(params: ITEM_STEAL_PARAMS): number {
const stackCount = params.pokemon.heldItemManager.getStack(this.type);
return randSeedFloat() <= this.chance * stackCount ? 1 : 0;
}
getTransferMessage(params: ITEM_STEAL_PARAMS, itemId: HeldItemId): string {
return i18next.t("modifier:contactHeldItemTransferApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(params.target),
itemName: allHeldItems[itemId].name,
pokemonName: params.pokemon.getNameToRender(),
typeName: this.name,
});
}
}

View File

@ -0,0 +1,89 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import { isNullOrUndefined, type NumberHolder } from "#app/utils/common";
import type { MoveId } from "#enums/move-id";
import { allMoves } from "#app/data/data-lists";
import i18next from "i18next";
export interface MULTI_HIT_PARAMS {
pokemon: Pokemon;
moveId: MoveId;
count?: NumberHolder;
damageMultiplier?: NumberHolder;
}
/**
* Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a
* set {@linkcode StatusEffect} at the end of a turn.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class MultiHitHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.MULTI_HIT];
get description(): string {
return i18next.t("modifierType:ModifierType.PokemonMultiHitModifierType.description");
}
/**
* For each stack, converts 25 percent of attack damage into an additional strike.
* @param pokemon The {@linkcode Pokemon} using the move
* @param moveId The {@linkcode MoveId | identifier} for the move being used
* @param count {@linkcode NumberHolder} holding the move's hit count for this turn
* @param damageMultiplier {@linkcode NumberHolder} holding a damage multiplier applied to a strike of this move
* @returns always `true`
*/
apply(params: MULTI_HIT_PARAMS): boolean {
const pokemon = params.pokemon;
const move = allMoves[params.moveId];
/**
* The move must meet Parental Bond's restrictions for this item
* to apply. This means
* - Only attacks are boosted
* - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted
* (though Multi-Lens can still affect moves boosted by Parental Bond)
* - Multi-target moves are not boosted *unless* they can only hit a single Pokemon
* - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted
*/
if (!move.canBeMultiStrikeEnhanced(pokemon)) {
return false;
}
if (!isNullOrUndefined(params.count)) {
return this.applyHitCountBoost(pokemon, params.count);
}
if (!isNullOrUndefined(params.damageMultiplier)) {
return this.applyDamageModifier(pokemon, params.damageMultiplier);
}
return false;
}
/** Adds strikes to a move equal to the number of stacked Multi-Lenses */
private applyHitCountBoost(pokemon: Pokemon, count: NumberHolder): boolean {
const stackCount = pokemon.heldItemManager.getStack(this.type);
count.value += stackCount;
return true;
}
/**
* If applied to the first hit of a move, sets the damage multiplier
* equal to (1 - the number of stacked Multi-Lenses).
* Additional strikes beyond that are given a 0.25x damage multiplier
*/
private applyDamageModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean {
const stackCount = pokemon.heldItemManager.getStack(this.type);
if (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount) {
// Reduce first hit by 25% for each stack count
damageMultiplier.value *= 1 - 0.25 * stackCount;
return true;
}
if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== stackCount + 1) {
// Deal 25% damage for each remaining Multi Lens hit
damageMultiplier.value *= 0.25;
return true;
}
// An extra hit not caused by Multi Lens -- assume it is Parental Bond
return false;
}
}

View File

@ -0,0 +1,32 @@
import type Pokemon from "#app/field/pokemon";
import type { NumberHolder } from "#app/utils/common";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
export interface NATURE_WEIGHT_BOOST_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
multiplier: NumberHolder;
}
export class NatureWeightBoosterHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.NATURE_WEIGHT_BOOSTER];
/**
* Applies {@linkcode PokemonNatureWeightModifier}
* @param _pokemon The {@linkcode Pokemon} to apply the nature weight to
* @param multiplier {@linkcode NumberHolder} holding the nature weight
* @returns `true` if multiplier was applied
*/
apply(params: NATURE_WEIGHT_BOOST_PARAMS): boolean {
const pokemon = params.pokemon;
const multiplier = params.multiplier;
const stackCount = pokemon.heldItemManager.getStack(this.type);
if (multiplier.value !== 1) {
multiplier.value += 0.1 * stackCount * (multiplier.value > 1 ? 1 : -1);
return true;
}
return false;
}
}

View File

@ -0,0 +1,66 @@
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { BATTLE_STATS } from "#enums/stat";
import i18next from "i18next";
import { ConsumableHeldItem, HELD_ITEM_EFFECT } from "../held-item";
import { getPokemonNameWithAffix } from "#app/messages";
export interface RESET_NEGATIVE_STAT_STAGE_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
/** Whether the move was used by a player pokemon */
isPlayer: boolean;
}
/**
* Modifier used for held items, namely White Herb, that restore adverse stat
* stages in battle.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class ResetNegativeStatStageHeldItem extends ConsumableHeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.RESET_NEGATIVE_STAT_STAGE];
get name(): string {
return i18next.t("modifierType:ModifierType.WHITE_HERB.name");
}
get description(): string {
return i18next.t("modifierType:ModifierType.WHITE_HERB.description");
}
get iconName(): string {
return "white_herb";
}
/**
* Goes through the holder's stat stages and, if any are negative, resets that
* stat stage back to 0.
* @param pokemon {@linkcode Pokemon} that holds the item
* @returns `true` if any stat stages were reset, false otherwise
*/
apply(params: RESET_NEGATIVE_STAT_STAGE_PARAMS): boolean {
const pokemon = params.pokemon;
const isPlayer = params.isPlayer;
let statRestored = false;
for (const s of BATTLE_STATS) {
if (pokemon.getStatStage(s) < 0) {
pokemon.setStatStage(s, 0);
statRestored = true;
}
}
if (statRestored) {
globalScene.phaseManager.queueMessage(
i18next.t("modifier:resetNegativeStatStageApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
typeName: this.name,
}),
);
this.consume(pokemon, isPlayer, true, false);
}
return statRestored;
}
}

View File

@ -0,0 +1,191 @@
import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
import type Pokemon from "#app/field/pokemon";
import type { NumberHolder } from "#app/utils/common";
import { HeldItemId } from "#enums/held-item-id";
import type { SpeciesId } from "#enums/species-id";
import type { Stat } from "#enums/stat";
import { HeldItem, HELD_ITEM_EFFECT } from "../held-item";
export interface STAT_BOOST_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
stat: Stat;
statValue: NumberHolder;
}
/**
* Modifier used for held items that Applies {@linkcode Stat} boost(s)
* using a multiplier.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class StatBoostHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.STAT_BOOST];
/** The stats that the held item boosts */
protected stats: Stat[];
/** The multiplier used to increase the relevant stat(s) */
protected multiplier: number;
constructor(type: HeldItemId, maxStackCount = 1, stats: Stat[], multiplier: number) {
super(type, maxStackCount);
this.stats = stats;
this.multiplier = multiplier;
}
/**
* Checks if the incoming stat is listed in {@linkcode stats}
* @param _pokemon the {@linkcode Pokemon} that holds the item
* @param _stat the {@linkcode Stat} to be boosted
* @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat
* @returns `true` if the stat could be boosted, false otherwise
*/
// override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean {
// return super.shouldApply(pokemon, stat, statValue) && this.stats.includes(stat);
// }
/**
* Boosts the incoming stat by a {@linkcode multiplier} if the stat is listed
* in {@linkcode stats}.
* @param _pokemon the {@linkcode Pokemon} that holds the item
* @param _stat the {@linkcode Stat} to be boosted
* @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat
* @returns `true` if the stat boost applies successfully, false otherwise
* @see shouldApply
*/
apply(params: STAT_BOOST_PARAMS): boolean {
params.statValue.value *= this.multiplier;
return true;
}
getMaxHeldItemCount(_pokemon: Pokemon): number {
return 1;
}
}
/**
* Modifier used for held items, specifically Eviolite, that apply
* {@linkcode Stat} boost(s) using a multiplier if the holder can evolve.
* @extends StatBoosterModifier
* @see {@linkcode apply}
*/
export class EvolutionStatBoostHeldItem extends StatBoostHeldItem {
/**
* Checks if the stat boosts can apply and if the holder is not currently
* Gigantamax'd.
* @param pokemon {@linkcode Pokemon} that holds the held item
* @param stat {@linkcode Stat} The {@linkcode Stat} to be boosted
* @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat
* @returns `true` if the stat boosts can be applied, false otherwise
*/
// override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean {
// return super.shouldApply(pokemon, stat, statValue) && !pokemon.isMax();
// }
/**
* Boosts the incoming stat value by a {@linkcode EvolutionStatBoosterModifier.multiplier} if the holder
* can evolve. Note that, if the holder is a fusion, they will receive
* only half of the boost if either of the fused members are fully
* evolved. However, if they are both unevolved, the full boost
* will apply.
* @param pokemon {@linkcode Pokemon} that holds the item
* @param _stat {@linkcode Stat} The {@linkcode Stat} to be boosted
* @param statValue{@linkcode NumberHolder} that holds the resulting value of the stat
* @returns `true` if the stat boost applies successfully, false otherwise
* @see shouldApply
*/
override apply(params: STAT_BOOST_PARAMS): boolean {
const pokemon = params.pokemon;
const isUnevolved = pokemon.getSpeciesForm(true).speciesId in pokemonEvolutions;
if (pokemon.isFusion() && pokemon.getFusionSpeciesForm(true).speciesId in pokemonEvolutions !== isUnevolved) {
// Half boost applied if pokemon is fused and either part of fusion is fully evolved
params.statValue.value *= 1 + (this.multiplier - 1) / 2;
return true;
}
if (isUnevolved && !pokemon.isMax()) {
// Full boost applied if holder is unfused and unevolved or, if fused, both parts of fusion are unevolved
return super.apply(params);
}
return false;
}
}
export type SpeciesStatBoosterItemId =
| typeof HeldItemId.LIGHT_BALL
| typeof HeldItemId.THICK_CLUB
| typeof HeldItemId.METAL_POWDER
| typeof HeldItemId.QUICK_POWDER
| typeof HeldItemId.DEEP_SEA_SCALE
| typeof HeldItemId.DEEP_SEA_TOOTH;
export const SPECIES_STAT_BOOSTER_ITEMS: SpeciesStatBoosterItemId[] = [
HeldItemId.LIGHT_BALL,
HeldItemId.THICK_CLUB,
HeldItemId.METAL_POWDER,
HeldItemId.QUICK_POWDER,
HeldItemId.DEEP_SEA_SCALE,
HeldItemId.DEEP_SEA_TOOTH,
];
/**
* Modifier used for held items that Applies {@linkcode Stat} boost(s) using a
* multiplier if the holder is of a specific {@linkcode SpeciesId}.
* @extends StatBoostHeldItem
* @see {@linkcode apply}
*/
export class SpeciesStatBoostHeldItem extends StatBoostHeldItem {
/** The species that the held item's stat boost(s) apply to */
public species: SpeciesId[];
constructor(
type: SpeciesStatBoosterItemId,
maxStackCount = 1,
stats: Stat[],
multiplier: number,
species: SpeciesId[],
) {
super(type, maxStackCount, stats, multiplier);
this.species = species;
}
/**
* Checks if the incoming stat is listed in {@linkcode stats} and if the holder's {@linkcode SpeciesId}
* (or its fused species) is listed in {@linkcode species}.
* @param pokemon {@linkcode Pokemon} that holds the item
* @param stat {@linkcode Stat} being checked at the time
* @param statValue {@linkcode NumberHolder} that holds the resulting value of the stat
* @returns `true` if the stat could be boosted, false otherwise
*/
// override shouldApply(pokemon: Pokemon, stat: Stat, statValue: NumberHolder): boolean {
// return (
// super.shouldApply(pokemon, stat, statValue) &&
// (this.species.includes(pokemon.getSpeciesForm(true).speciesId) ||
// (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId)))
// );
// }
apply(params: STAT_BOOST_PARAMS): boolean {
const pokemon = params.pokemon;
const fitsSpecies =
this.species.includes(pokemon.getSpeciesForm(true).speciesId) ||
(pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId));
if (fitsSpecies) {
return super.apply(params);
}
return false;
}
/**
* Checks if either parameter is included in the corresponding lists
* @param speciesId {@linkcode SpeciesId} being checked
* @param stat {@linkcode Stat} being checked
* @returns `true` if both parameters are in {@linkcode species} and {@linkcode stats} respectively, false otherwise
*/
contains(speciesId: SpeciesId, stat: Stat): boolean {
return this.species.includes(speciesId) && this.stats.includes(stat);
}
}

View File

@ -0,0 +1,57 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import type { BooleanHolder } from "#app/utils/common";
import { globalScene } from "#app/global-scene";
import i18next from "i18next";
import { getPokemonNameWithAffix } from "#app/messages";
export interface SURVIVE_CHANCE_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
surviveDamage: BooleanHolder;
}
/**
* Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a
* set {@linkcode StatusEffect} at the end of a turn.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class SurviveChanceHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.SURVIVE_CHANCE];
/**
* Checks if the {@linkcode SurviveDamageModifier} should be applied
* @param pokemon the {@linkcode Pokemon} that holds the item
* @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage
* @returns `true` if the {@linkcode SurviveDamageModifier} should be applied
*/
// override shouldApply(pokemon?: Pokemon, surviveDamage?: BooleanHolder): boolean {
// return super.shouldApply(pokemon, surviveDamage) && !!surviveDamage;
// }
/**
* Applies {@linkcode SurviveDamageModifier}
* @param pokemon the {@linkcode Pokemon} that holds the item
* @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage
* @returns `true` if the survive damage has been applied
*/
apply(params: SURVIVE_CHANCE_PARAMS): boolean {
const pokemon = params.pokemon;
const surviveDamage = params.surviveDamage;
const stackCount = pokemon.heldItemManager.getStack(this.type);
if (!surviveDamage.value && pokemon.randBattleSeedInt(10) < stackCount) {
surviveDamage.value = true;
globalScene.phaseManager.queueMessage(
i18next.t("modifier:surviveDamageApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
typeName: this.name,
}),
);
return true;
}
return false;
}
}

View File

@ -0,0 +1,36 @@
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import i18next from "i18next";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { toDmgValue } from "#app/utils/common";
import { getPokemonNameWithAffix } from "#app/messages";
export interface TURN_END_HEAL_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
}
export class TurnEndHealHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_HEAL];
apply(params: TURN_END_HEAL_PARAMS): boolean {
const pokemon = params.pokemon;
const stackCount = pokemon.heldItemManager.getStack(this.type);
if (pokemon.isFullHp()) {
return false;
}
globalScene.phaseManager.unshiftPhase(
new PokemonHealPhase(
pokemon.getBattlerIndex(),
toDmgValue(pokemon.getMaxHp() / 16) * stackCount,
i18next.t("modifier:turnHealApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
typeName: this.name,
}),
true,
),
);
return true;
}
}

View File

@ -0,0 +1,40 @@
import type Pokemon from "#app/field/pokemon";
import { HeldItem, HELD_ITEM_EFFECT } from "#app/items/held-item";
import type { StatusEffect } from "#enums/status-effect";
import type { HeldItemId } from "#enums/held-item-id";
export interface TURN_END_STATUS_PARAMS {
/** The pokemon with the item */
pokemon: Pokemon;
}
/**
* Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a
* set {@linkcode StatusEffect} at the end of a turn.
* @extends PokemonHeldItemModifier
* @see {@linkcode apply}
*/
export class TurnEndStatusHeldItem extends HeldItem {
public effects: HELD_ITEM_EFFECT[] = [HELD_ITEM_EFFECT.TURN_END_STATUS];
/** The status effect to be applied by the held item */
public effect: StatusEffect;
constructor(type: HeldItemId, maxStackCount = 1, effect: StatusEffect) {
super(type, maxStackCount);
this.effect = effect;
}
/**
* Tries to inflicts the holder with the associated {@linkcode StatusEffect}.
* @param pokemon {@linkcode Pokemon} that holds the held item
* @returns `true` if the status effect was applied successfully
*/
apply(params: TURN_END_STATUS_PARAMS): boolean {
return params.pokemon.trySetStatus(this.effect, true, undefined, undefined, this.name);
}
getStatusEffect(): StatusEffect {
return this.effect;
}
}

View File

@ -0,0 +1,80 @@
import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { RewardTier } from "#enums/reward-tier";
import { dailyStarterHeldItemPool, trainerHeldItemPool, wildHeldItemPool } from "./held-item-pool";
/**
* Initialize the wild held item pool
*/
function initWildHeldItemPool() {
wildHeldItemPool[RewardTier.COMMON] = [{ entry: HeldItemCategoryId.BERRY, weight: 1 }];
wildHeldItemPool[RewardTier.GREAT] = [{ entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 1 }];
wildHeldItemPool[RewardTier.ULTRA] = [
{ entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 5 },
{ entry: HeldItemId.WHITE_HERB, weight: 0 },
];
wildHeldItemPool[RewardTier.ROGUE] = [{ entry: HeldItemId.LUCKY_EGG, weight: 4 }];
wildHeldItemPool[RewardTier.MASTER] = [{ entry: HeldItemId.GOLDEN_EGG, weight: 1 }];
}
/**
* Initialize the trainer pokemon held item pool
*/
function initTrainerHeldItemPool() {
trainerHeldItemPool[RewardTier.COMMON] = [
{ entry: HeldItemCategoryId.BERRY, weight: 8 },
{ entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 3 },
];
trainerHeldItemPool[RewardTier.GREAT] = [{ entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 3 }];
trainerHeldItemPool[RewardTier.ULTRA] = [
{ entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 10 },
{ entry: HeldItemId.WHITE_HERB, weight: 0 },
];
trainerHeldItemPool[RewardTier.ROGUE] = [
{ entry: HeldItemId.FOCUS_BAND, weight: 2 },
{ entry: HeldItemId.LUCKY_EGG, weight: 4 },
{ entry: HeldItemId.QUICK_CLAW, weight: 1 },
{ entry: HeldItemId.GRIP_CLAW, weight: 1 },
{ entry: HeldItemId.WIDE_LENS, weight: 1 },
];
trainerHeldItemPool[RewardTier.MASTER] = [
{ entry: HeldItemId.KINGS_ROCK, weight: 1 },
{ entry: HeldItemId.LEFTOVERS, weight: 1 },
{ entry: HeldItemId.SHELL_BELL, weight: 1 },
{ entry: HeldItemId.SCOPE_LENS, weight: 1 },
];
}
/**
* Initialize the daily starter held item pool
*/
function initDailyStarterModifierPool() {
dailyStarterHeldItemPool[RewardTier.COMMON] = [
{ entry: HeldItemCategoryId.BASE_STAT_BOOST, weight: 1 },
{ entry: HeldItemCategoryId.BERRY, weight: 3 },
];
dailyStarterHeldItemPool[RewardTier.GREAT] = [{ entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 5 }];
dailyStarterHeldItemPool[RewardTier.ULTRA] = [
{ entry: HeldItemId.REVIVER_SEED, weight: 4 },
{ entry: HeldItemId.SOOTHE_BELL, weight: 1 },
{ entry: HeldItemId.SOUL_DEW, weight: 1 },
{ entry: HeldItemId.GOLDEN_PUNCH, weight: 1 },
];
dailyStarterHeldItemPool[RewardTier.ROGUE] = [
{ entry: HeldItemId.GRIP_CLAW, weight: 5 },
{ entry: HeldItemId.BATON, weight: 2 },
{ entry: HeldItemId.FOCUS_BAND, weight: 5 },
{ entry: HeldItemId.QUICK_CLAW, weight: 3 },
{ entry: HeldItemId.KINGS_ROCK, weight: 3 },
];
dailyStarterHeldItemPool[RewardTier.MASTER] = [
{ entry: HeldItemId.LEFTOVERS, weight: 1 },
{ entry: HeldItemId.SHELL_BELL, weight: 1 },
];
}
export function initHeldItemPools() {
// Default held item pools for specific scenarios
initWildHeldItemPool();
initTrainerHeldItemPool();
initDailyStarterModifierPool();
}

View File

@ -0,0 +1,41 @@
import { RewardTier } from "#enums/reward-tier";
import { TrainerItemId } from "#enums/trainer-item-id";
import { enemyBuffTokenPool } from "./trainer-item-pool";
/**
* Initialize the enemy buff modifier pool
*/
function initEnemyBuffTokenPool() {
enemyBuffTokenPool[RewardTier.COMMON] = [
{ entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 9 },
{ entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 9 },
{ entry: TrainerItemId.ENEMY_ATTACK_POISON_CHANCE, weight: 3 },
{ entry: TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE, weight: 3 },
{ entry: TrainerItemId.ENEMY_ATTACK_BURN_CHANCE, weight: 3 },
{ entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 9 },
{ entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 4 },
{ entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 1 },
];
enemyBuffTokenPool[RewardTier.GREAT] = [
{ entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 5 },
{ entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 5 },
{ entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 5 },
{ entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 5 },
{ entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 1 },
];
enemyBuffTokenPool[RewardTier.ULTRA] = [
{ entry: TrainerItemId.ENEMY_DAMAGE_BOOSTER, weight: 10 },
{ entry: TrainerItemId.ENEMY_DAMAGE_REDUCTION, weight: 10 },
{ entry: TrainerItemId.ENEMY_HEAL, weight: 10 },
{ entry: TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE, weight: 10 },
{ entry: TrainerItemId.ENEMY_ENDURE_CHANCE, weight: 10 },
{ entry: TrainerItemId.ENEMY_FUSED_CHANCE, weight: 5 },
];
enemyBuffTokenPool[RewardTier.ROGUE] = [];
enemyBuffTokenPool[RewardTier.MASTER] = [];
}
export function initTrainerItemPools() {
// Default held item pools for specific scenarios
initEnemyBuffTokenPool();
}

41
src/items/item-utility.ts Normal file
View File

@ -0,0 +1,41 @@
import { allHeldItems, allTrainerItems } from "#app/data/data-lists";
import { formChangeItemName } from "#app/data/pokemon-forms";
import type { FormChangeItem } from "#enums/form-change-item";
import type { HeldItemId } from "#enums/held-item-id";
import type { TrainerItemId } from "#enums/trainer-item-id";
export const trainerItemSortFunc = (a: TrainerItemId, b: TrainerItemId): number => {
const itemNameMatch = allTrainerItems[a].name.localeCompare(allTrainerItems[b].name);
const itemIdMatch = a - b;
if (itemIdMatch === 0) {
return itemNameMatch;
//Finally sort by item name
}
return itemIdMatch;
};
//TODO: revisit this function
export const heldItemSortFunc = (a: HeldItemId, b: HeldItemId): number => {
const itemNameMatch = allHeldItems[a].name.localeCompare(allHeldItems[b].name);
const itemIdMatch = a - b;
if (itemIdMatch === 0) {
return itemNameMatch;
//Finally sort by item name
}
return itemIdMatch;
};
export const formChangeItemSortFunc = (a: FormChangeItem, b: FormChangeItem): number => {
const nameA = formChangeItemName(a);
const nameB = formChangeItemName(b);
const itemNameMatch = nameA.localeCompare(nameB);
const itemIdMatch = a - b;
if (itemIdMatch === 0) {
return itemNameMatch;
//Finally sort by item name
}
return itemIdMatch;
};

View File

@ -0,0 +1,152 @@
import type ModifierData from "#app/system/modifier-data";
import type { BerryType } from "#enums/berry-type";
import { HeldItemId } from "#enums/held-item-id";
import type { PokemonType } from "#enums/pokemon-type";
import { Stat, type PermanentStat } from "#enums/stat";
import type { PokemonItemMap } from "./held-item-data-types";
import { attackTypeToHeldItem } from "./held-items/attack-type-booster";
import { permanentStatToHeldItem } from "./held-items/base-stat-booster";
import { berryTypeToHeldItem } from "./held-items/berry";
import { SpeciesId } from "#enums/species-id";
const uniqueModifierToItem = {
EvoTrackerModifier: HeldItemId.GIMMIGHOUL_EVO_TRACKER,
PokemonBaseStatFlatModifier: HeldItemId.OLD_GATEAU,
PokemonIncrementingStatModifier: HeldItemId.MACHO_BRACE,
SurviveDamageModifier: HeldItemId.FOCUS_BAND,
BypassSpeedChanceModifier: HeldItemId.QUICK_CLAW,
FlinchChanceModifier: HeldItemId.KINGS_ROCK,
TurnHealModifier: HeldItemId.LEFTOVERS,
HitHealModifier: HeldItemId.SHELL_BELL,
PokemonInstantReviveModifier: HeldItemId.REVIVER_SEED,
ResetNegativeStatStageModifier: HeldItemId.WHITE_HERB,
FieldEffectModifier: HeldItemId.MYSTICAL_ROCK,
PokemonFriendshipBoosterModifier: HeldItemId.SOOTHE_BELL,
PokemonNatureWeightModifier: HeldItemId.SOUL_DEW,
PokemonMoveAccuracyBoosterModifier: HeldItemId.WIDE_LENS,
PokemonMultiHitModifier: HeldItemId.MULTI_LENS,
DamageMoneyRewardModifier: HeldItemId.GOLDEN_PUNCH,
SwitchEffectTransferModifier: HeldItemId.BATON,
TurnHeldItemTransferModifier: HeldItemId.MINI_BLACK_HOLE,
ContactHeldItemTransferChanceModifier: HeldItemId.GRIP_CLAW,
} as const;
type UniqueModifierString = keyof typeof uniqueModifierToItem;
function isUniqueModifierString(value: string): value is UniqueModifierString {
return value in uniqueModifierToItem;
}
const modifierCategoryList = [
"BaseStatModifier",
"EvolutionStatBoosterModifier",
"SpeciesStatBoosterModifier",
"CritBoosterModifier",
"SpeciesCritBoosterModifier",
"AttackTypeBoosterModifier",
"TurnStatusEffectModifier",
"BerryModifier",
"PokemonExpBoosterModifier",
"PokemonFormChangeItemModifier",
"PokemonBaseStatTotalModifier",
] as const;
type ModifierCategoryString = (typeof modifierCategoryList)[number];
function isModifierCategoryString(value: string): value is ModifierCategoryString {
return modifierCategoryList.includes(value as ModifierCategoryString);
}
function mapModifierCategoryToItems(modifier: ModifierCategoryString, typeId: string, args: any): HeldItemId {
if (modifier === "BaseStatModifier") {
const stat = args[1] as PermanentStat;
return permanentStatToHeldItem[stat];
}
if (modifier === "EvolutionStatBoosterModifier") {
return HeldItemId.EVIOLITE;
}
if (modifier === "SpeciesStatBoosterModifier") {
const stats = args[1];
const species = args[3];
if (SpeciesId.PIKACHU in species) {
return HeldItemId.LIGHT_BALL;
}
if (SpeciesId.CUBONE in species) {
return HeldItemId.THICK_CLUB;
}
if (SpeciesId.DITTO in species && Stat.DEF in stats) {
return HeldItemId.METAL_POWDER;
}
if (SpeciesId.DITTO in species && Stat.SPDEF in stats) {
return HeldItemId.QUICK_POWDER;
}
if (SpeciesId.CLAMPERL in species && Stat.SPDEF in stats) {
return HeldItemId.DEEP_SEA_SCALE;
}
if (SpeciesId.CLAMPERL in species && Stat.SPATK in stats) {
return HeldItemId.DEEP_SEA_TOOTH;
}
}
if (modifier === "CritBoosterModifier") {
return HeldItemId.SCOPE_LENS;
}
if (modifier === "SpeciesCritBoosterModifier") {
return HeldItemId.LEEK;
}
if (modifier === "AttackTypeBoosterModifier") {
const moveType = args[1] as PokemonType;
return attackTypeToHeldItem[moveType];
}
if (modifier === "TurnStatusEffectModifier") {
switch (typeId) {
case "TOXIC_ORB":
return HeldItemId.TOXIC_ORB;
case "FLAME_ORB":
return HeldItemId.FLAME_ORB;
}
}
if (modifier === "BerryModifier") {
const berryType = args[1] as BerryType;
return berryTypeToHeldItem[berryType];
}
if (modifier === "PokemonExpBoosterModifier") {
const boost = args[1] as number;
return boost === 100 ? HeldItemId.GOLDEN_EGG : HeldItemId.LUCKY_EGG;
}
if (modifier === "PokemonBaseStatTotalModifier") {
const statModifier = args[1] as number;
return statModifier > 0 ? HeldItemId.SHUCKLE_JUICE_GOOD : HeldItemId.SHUCKLE_JUICE_BAD;
}
return 0;
}
export function convertModifierSaveData(data: ModifierData[]) {
const pokemonItems: PokemonItemMap[] = [];
for (const entry of data) {
const typeId = entry.typeId;
const args = entry.args;
const pokemonId = args[0];
const stack = entry.stackCount;
const className = entry.className;
if (className === "PokemonFormChangeItemModifier") {
}
//TODO: Code to filter out modifiers which are not held items
let itemId: HeldItemId = 0;
if (isModifierCategoryString(className)) {
itemId = mapModifierCategoryToItems(className, typeId, args);
}
if (isUniqueModifierString(className)) {
itemId = uniqueModifierToItem[className];
}
if (itemId) {
const specs = { id: itemId, stack: stack };
const pokemonItem = { item: specs, pokemonId: pokemonId };
pokemonItems.push(pokemonItem);
}
}
}

View File

@ -0,0 +1,44 @@
import type { RewardTier } from "#enums/reward-tier";
import type { TrainerItemId } from "#enums/trainer-item-id";
export type TrainerItemData = {
stack: number;
disabled?: boolean;
cooldown?: number;
};
export type TrainerItemDataMap = {
[key in TrainerItemId]?: TrainerItemData;
};
export type TrainerItemSpecs = TrainerItemData & {
id: TrainerItemId;
};
export function isTrainerItemSpecs(entry: any): entry is TrainerItemSpecs {
return typeof entry.id === "number" && "stack" in entry;
}
type TrainerItemPoolEntry = {
entry: TrainerItemId;
weight: number;
};
export type TrainerItemPool = TrainerItemPoolEntry[];
export type TrainerItemTieredPool = {
[key in RewardTier]?: TrainerItemPool;
};
export function isTrainerItemPool(value: any): value is TrainerItemPool {
return Array.isArray(value) && value.every(entry => "entry" in entry && "weight" in entry);
}
type TrainerItemConfigurationEntry = {
entry: TrainerItemId | TrainerItemSpecs;
count?: number | (() => number);
};
export type TrainerItemConfiguration = TrainerItemConfigurationEntry[];
export type TrainerItemSaveData = TrainerItemSpecs[];

View File

@ -0,0 +1,158 @@
import { allTrainerItems } from "#app/data/data-lists";
import type { TrainerItemId } from "#app/enums/trainer-item-id";
import {
isTrainerItemSpecs,
type TrainerItemSaveData,
type TrainerItemConfiguration,
type TrainerItemDataMap,
type TrainerItemSpecs,
} from "#app/items/trainer-item-data-types";
export class TrainerItemManager {
public trainerItems: TrainerItemDataMap;
constructor() {
this.trainerItems = {};
}
getItemSpecs(id: TrainerItemId): TrainerItemSpecs | undefined {
const item = this.trainerItems[id];
if (item) {
const itemSpecs: TrainerItemSpecs = {
...item,
id,
};
return itemSpecs;
}
return undefined;
}
generateTrainerItemConfiguration(restrictedIds?: TrainerItemId[]): TrainerItemConfiguration {
const config: TrainerItemConfiguration = [];
for (const [k, item] of Object.entries(this.trainerItems)) {
const id = Number(k);
if (item && (!restrictedIds || id in restrictedIds)) {
const specs: TrainerItemSpecs = { ...item, id };
config.push({ entry: specs, count: 1 });
}
}
return config;
}
generateSaveData(): TrainerItemSaveData {
const saveData: TrainerItemSaveData = [];
for (const [k, item] of Object.entries(this.trainerItems)) {
const id = Number(k);
if (item) {
const specs: TrainerItemSpecs = { ...item, id };
saveData.push(specs);
}
}
return saveData;
}
getTrainerItems(): number[] {
return Object.keys(this.trainerItems).map(k => Number(k));
}
hasItem(itemType: TrainerItemId): boolean {
return itemType in this.trainerItems;
}
getStack(itemType: TrainerItemId): number {
const item = this.trainerItems[itemType];
return item ? item.stack : 0;
}
isMaxStack(itemType: TrainerItemId): boolean {
const item = this.trainerItems[itemType];
return item ? item.stack >= allTrainerItems[itemType].getMaxStackCount() : false;
}
overrideItems(newItems: TrainerItemDataMap) {
this.trainerItems = newItems;
// The following is to allow randomly generated item configs to have stack 0
for (const [item, properties] of Object.entries(this.trainerItems)) {
if (!properties || properties.stack <= 0) {
delete this.trainerItems[item];
}
}
}
add(itemType: TrainerItemId | TrainerItemSpecs, addStack?: number): boolean {
if (isTrainerItemSpecs(itemType)) {
return this.addItemWithSpecs(itemType);
}
if (!addStack) {
addStack = allTrainerItems[itemType].isLapsing ? allTrainerItems[itemType].getMaxStackCount() : 1;
}
const maxStack = allTrainerItems[itemType].getMaxStackCount();
const item = this.trainerItems[itemType];
if (item) {
// TODO: We may want an error message of some kind instead
if (item.stack < maxStack) {
item.stack = Math.min(item.stack + addStack, maxStack);
return true;
}
} else {
this.trainerItems[itemType] = { stack: Math.min(addStack, maxStack) };
return true;
}
return false;
}
addItemWithSpecs(itemSpecs: TrainerItemSpecs): boolean {
const id = itemSpecs.id;
const maxStack = allTrainerItems[id].getMaxStackCount();
const item = this.trainerItems[id];
const tempStack = item?.stack ?? 0;
this.trainerItems[id] = itemSpecs;
this.trainerItems[id].stack = Math.min(itemSpecs.stack + tempStack, maxStack);
return true;
}
remove(itemType: TrainerItemId, removeStack = 1, all = false) {
const item = this.trainerItems[itemType];
if (item) {
item.stack -= removeStack;
if (all || item.stack <= 0) {
delete this.trainerItems[itemType];
}
}
}
filterRequestedItems(requestedItems: TrainerItemId[], exclude = false) {
const currentItems = this.getTrainerItems();
return currentItems.filter(it => !exclude && requestedItems.some(entry => it === entry));
}
getTrainerItemCount(): number {
let total = 0;
for (const properties of Object.values(this.trainerItems)) {
total += properties?.stack ?? 0;
}
return total;
}
lapseItems(): void {
for (const [item, properties] of Object.entries(this.trainerItems)) {
if (allTrainerItems[item].isLapsing && properties) {
properties.stack -= 1;
}
if (!properties || properties.stack <= 0) {
delete this.trainerItems[item];
}
}
}
clearItems() {
this.trainerItems = {};
}
}

View File

@ -0,0 +1,62 @@
import { globalScene } from "#app/global-scene";
import { isNullOrUndefined, pickWeightedIndex } from "#app/utils/common";
import { RewardTier } from "#enums/reward-tier";
import type { TrainerItemId } from "#enums/trainer-item-id";
import { allTrainerItems } from "./all-trainer-items";
import type { TrainerItemPool, TrainerItemTieredPool } from "./trainer-item-data-types";
import type { TrainerItemManager } from "./trainer-item-manager";
export const enemyBuffTokenPool: TrainerItemTieredPool = {};
function getPoolWeights(pool: TrainerItemPool, manager: TrainerItemManager): number[] {
return pool.map(p => {
if (manager.isMaxStack(p.entry)) {
return 0;
}
return p.weight;
});
}
export function getNewTrainerItemFromPool(pool: TrainerItemPool, manager: TrainerItemManager): TrainerItemId {
const weights = getPoolWeights(pool, manager);
const pickedIndex = pickWeightedIndex(weights);
if (isNullOrUndefined(pickedIndex)) {
return 0;
}
const entry = pool[pickedIndex].entry;
return entry as TrainerItemId;
}
export function assignEnemyBuffTokenForWave(tier: RewardTier) {
let tierStackCount: number;
switch (tier) {
case RewardTier.ULTRA:
tierStackCount = 5;
break;
case RewardTier.GREAT:
tierStackCount = 3;
break;
default:
tierStackCount = 1;
break;
}
if (!enemyBuffTokenPool[tier]) {
return;
}
const retryCount = 50;
let candidate = getNewTrainerItemFromPool(enemyBuffTokenPool[tier], globalScene.enemyTrainerItems);
let r = 0;
while (
++r < retryCount &&
allTrainerItems[candidate].getMaxStackCount() <
globalScene.enemyTrainerItems.getStack(candidate) + (r < 10 ? tierStackCount : 1)
) {
candidate = getNewTrainerItemFromPool(enemyBuffTokenPool[tier], globalScene.enemyTrainerItems);
}
globalScene.enemyTrainerItems.add(candidate, tierStackCount);
}

575
src/items/trainer-item.ts Normal file
View File

@ -0,0 +1,575 @@
import type Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { hslToHex, randSeedFloat, toDmgValue, type BooleanHolder, type NumberHolder } from "#app/utils/common";
import { TrainerItemId, TrainerItemNames } from "#enums/trainer-item-id";
import i18next from "i18next";
import type { TrainerItemManager } from "./trainer-item-manager";
import { addTextObject, TextStyle } from "#app/ui/text";
import { getStatKey, Stat, type TempBattleStat } from "#enums/stat";
import { BattlerTagType } from "#enums/battler-tag-type";
import { getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect";
import { getPokemonNameWithAffix } from "#app/messages";
import { StatusEffect } from "#enums/status-effect";
export const TRAINER_ITEM_EFFECT = {
LEVEL_INCREMENT_BOOSTER: 1,
PRESERVE_BERRY: 2,
HEALING_BOOSTER: 3,
EXP_BOOSTER: 4,
MONEY_MULTIPLIER: 5,
HIDDEN_ABILITY_CHANCE_BOOSTER: 6,
SHINY_RATE_BOOSTER: 7,
CRITICAL_CATCH_CHANCE_BOOSTER: 8,
EXTRA_REWARD: 9,
HEAL_SHOP_COST: 10,
DOUBLE_BATTLE_CHANCE_BOOSTER: 11,
TEMP_STAT_STAGE_BOOSTER: 12,
TEMP_ACCURACY_BOOSTER: 13,
TEMP_CRIT_BOOSTER: 14,
ENEMY_DAMAGE_BOOSTER: 15,
ENEMY_DAMAGE_REDUCER: 16,
ENEMY_HEAL: 17,
ENEMY_ATTACK_STATUS_CHANCE: 18,
ENEMY_STATUS_HEAL_CHANCE: 19,
ENEMY_ENDURE_CHANCE: 20,
ENEMY_FUSED_CHANCE: 21,
} as const;
export type TRAINER_ITEM_EFFECT = (typeof TRAINER_ITEM_EFFECT)[keyof typeof TRAINER_ITEM_EFFECT];
export interface NUMBER_HOLDER_PARAMS {
numberHolder: NumberHolder;
}
export interface BOOLEAN_HOLDER_PARAMS {
booleanHolder: BooleanHolder;
}
export interface POKEMON_PARAMS {
pokemon: Pokemon;
}
export class TrainerItem {
// public pokemonId: number;
public type: TrainerItemId;
public maxStackCount: number;
public isLapsing = false;
public effects: TRAINER_ITEM_EFFECT[] = [];
//TODO: If this is actually never changed by any subclass, perhaps it should not be here
public soundName = "se/restore";
constructor(type: TrainerItemId, maxStackCount = 1) {
this.type = type;
this.maxStackCount = maxStackCount;
}
get name(): string {
return i18next.t(`modifierType:ModifierType.${TrainerItemNames[this.type]}.name`);
}
get description(): string {
return i18next.t(`modifierType:ModifierType.${TrainerItemNames[this.type]}.description`);
}
get iconName(): string {
return `${TrainerItemNames[this.type]?.toLowerCase()}`;
}
getMaxStackCount(): number {
return this.maxStackCount;
}
createIcon(stackCount: number): Phaser.GameObjects.Container {
const container = globalScene.add.container(0, 0);
const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5);
container.add(item);
const stackText = this.getIconStackText(stackCount);
if (stackText) {
container.add(stackText);
}
return container;
}
getIconStackText(stackCount: number): Phaser.GameObjects.BitmapText | null {
if (this.getMaxStackCount() === 1 || stackCount < 1) {
return null;
}
const text = globalScene.add.bitmapText(10, 15, "item-count", stackCount.toString(), 11);
text.letterSpacing = -0.5;
if (stackCount >= this.getMaxStackCount()) {
text.setTint(0xf89890);
}
text.setOrigin(0);
return text;
}
getScoreMultiplier(): number {
return 1;
}
}
// Candy Jar
export class LevelIncrementBoosterTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.LEVEL_INCREMENT_BOOSTER];
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const count = params.numberHolder;
const stack = manager.getStack(this.type);
count.value += stack;
}
}
// Berry Pouch
export interface PRESERVE_BERRY_PARAMS {
pokemon: Pokemon;
doPreserve: BooleanHolder;
}
export class PreserveBerryTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.PRESERVE_BERRY];
apply(manager: TrainerItemManager, params: PRESERVE_BERRY_PARAMS) {
const stack = manager.getStack(this.type);
params.doPreserve.value ||= params.pokemon.randBattleSeedInt(10) < stack * 3;
}
}
// Healing Charm
export class HealingBoosterTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.HEALING_BOOSTER];
private multiplier: number;
constructor(type: TrainerItemId, multiplier: number, stackCount?: number) {
super(type, stackCount);
this.multiplier = multiplier;
}
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const healingMultiplier = params.numberHolder;
const stack = manager.getStack(this.type);
healingMultiplier.value *= 1 + (this.multiplier - 1) * stack;
}
}
// Exp Booster
export class ExpBoosterTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.EXP_BOOSTER];
private boostPercent: number;
constructor(type: TrainerItemId, boostPercent: number, stackCount?: number) {
super(type, stackCount);
this.boostPercent = boostPercent;
}
get description(): string {
return i18next.t("modifierType:ModifierType.ExpBoosterModifierType.description", {
boostPercent: this.boostPercent,
});
}
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const boost = params.numberHolder;
const stack = manager.getStack(this.type);
boost.value = Math.floor(boost.value * (1 + stack * this.boostPercent * 0.01));
}
}
export class MoneyMultiplierTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.MONEY_MULTIPLIER];
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const moneyMultiplier = params.numberHolder;
const stack = manager.getStack(this.type);
moneyMultiplier.value += Math.floor(moneyMultiplier.value * 0.2 * stack);
}
}
export class HiddenAbilityChanceBoosterTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.HIDDEN_ABILITY_CHANCE_BOOSTER];
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const boost = params.numberHolder;
const stack = manager.getStack(this.type);
boost.value *= Math.pow(2, -1 - stack);
}
}
export class ShinyRateBoosterTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.SHINY_RATE_BOOSTER];
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const boost = params.numberHolder;
const stack = manager.getStack(this.type);
boost.value *= Math.pow(2, 1 + stack);
}
}
export class CriticalCatchChanceBoosterTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.CRITICAL_CATCH_CHANCE_BOOSTER];
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const boost = params.numberHolder;
const stack = manager.getStack(this.type);
boost.value *= 1.5 + stack / 2;
}
}
export class ExtraRewardTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.EXTRA_REWARD];
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const count = params.numberHolder;
const stack = manager.getStack(this.type);
count.value += stack;
}
}
export class HealShopCostTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.HEAL_SHOP_COST];
public readonly shopMultiplier: number;
constructor(type: TrainerItemId, shopMultiplier: number, stackCount?: number) {
super(type, stackCount);
this.shopMultiplier = shopMultiplier;
}
apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const moneyCost = params.numberHolder;
moneyCost.value = Math.floor(moneyCost.value * this.shopMultiplier);
}
}
export class LapsingTrainerItem extends TrainerItem {
isLapsing = true;
createIcon(battleCount: number): Phaser.GameObjects.Container {
const container = globalScene.add.container(0, 0);
const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5);
container.add(item);
// Linear interpolation on hue
const hue = Math.floor(120 * (battleCount / this.getMaxStackCount()) + 5);
// Generates the color hex code with a constant saturation and lightness but varying hue
const typeHex = hslToHex(hue, 0.5, 0.9);
const strokeHex = hslToHex(hue, 0.7, 0.3);
const battleCountText = addTextObject(27, 0, battleCount.toString(), TextStyle.PARTY, {
fontSize: "66px",
color: typeHex,
});
battleCountText.setShadow(0, 0);
battleCountText.setStroke(strokeHex, 16);
battleCountText.setOrigin(1, 0);
container.add(battleCountText);
return container;
}
}
export class DoubleBattleChanceBoosterTrainerItem extends LapsingTrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.DOUBLE_BATTLE_CHANCE_BOOSTER];
get description(): string {
return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", {
battleCount: this.getMaxStackCount(),
});
}
apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const doubleBattleChance = params.numberHolder;
// This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using randSeedInt
// A double battle will initiate if the generated number is 0
doubleBattleChance.value /= 4;
}
}
interface TempStatToTrainerItemMap {
[key: number]: TrainerItemId;
}
export const tempStatToTrainerItem: TempStatToTrainerItemMap = {
[Stat.ATK]: TrainerItemId.X_ATTACK,
[Stat.DEF]: TrainerItemId.X_DEFENSE,
[Stat.SPATK]: TrainerItemId.X_SP_ATK,
[Stat.SPDEF]: TrainerItemId.X_SP_DEF,
[Stat.SPD]: TrainerItemId.X_SPEED,
[Stat.ACC]: TrainerItemId.X_ACCURACY,
};
export class TempStatStageBoosterTrainerItem extends LapsingTrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.TEMP_STAT_STAGE_BOOSTER];
private stat: TempBattleStat;
constructor(type: TrainerItemId, stat: TempBattleStat, stackCount?: number) {
super(type, stackCount);
this.stat = stat;
}
get name(): string {
return i18next.t(`modifierType:TempStatStageBoosterItem.${TrainerItemNames[this.type]?.toLowerCase()}`);
}
get description(): string {
console.log();
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", {
stat: i18next.t(getStatKey(this.stat)),
amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.percentage"),
});
}
apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const statLevel = params.numberHolder;
const boost = 0.3;
statLevel.value += boost;
}
}
export class TempAccuracyBoosterTrainerItem extends LapsingTrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.TEMP_ACCURACY_BOOSTER];
get name(): string {
return i18next.t(`modifierType:TempStatStageBoosterItem.${TrainerItemNames[this.type]?.toLowerCase()}`);
}
get description(): string {
console.log();
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", {
stat: i18next.t(getStatKey(Stat.ACC)),
amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.percentage"),
});
}
apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const statLevel = params.numberHolder;
const boost = 1;
statLevel.value += boost;
}
}
export class TempCritBoosterTrainerItem extends LapsingTrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.TEMP_CRIT_BOOSTER];
get description(): string {
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", {
stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises"),
amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.stage"),
});
}
apply(_manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS) {
const critLevel = params.numberHolder;
critLevel.value++;
}
}
export class EnemyDamageBoosterTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_BOOSTER];
public damageBoost = 1.05;
get iconName(): string {
return "wl_item_drop";
}
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS): boolean {
const stack = manager.getStack(this.type);
const multiplier = params.numberHolder;
multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageBoost, stack));
return true;
}
getMaxStackCount(): number {
return 999;
}
}
export class EnemyDamageReducerTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_DAMAGE_REDUCER];
public damageReduction = 0.975;
get iconName(): string {
return "wl_guard_spec";
}
apply(manager: TrainerItemManager, params: NUMBER_HOLDER_PARAMS): boolean {
const stack = manager.getStack(this.type);
const multiplier = params.numberHolder;
multiplier.value = toDmgValue(multiplier.value * Math.pow(this.damageReduction, stack));
return true;
}
getMaxStackCount(): number {
return globalScene.currentBattle.waveIndex < 2000 ? 99 : 999;
}
}
export class EnemyTurnHealTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_HEAL];
public healPercent = 2;
get iconName(): string {
return "wl_potion";
}
apply(manager: TrainerItemManager, params: POKEMON_PARAMS): boolean {
const stack = manager.getStack(this.type);
const enemyPokemon = params.pokemon;
if (!enemyPokemon.isFullHp()) {
globalScene.phaseManager.unshiftNew(
"PokemonHealPhase",
enemyPokemon.getBattlerIndex(),
Math.max(Math.floor(enemyPokemon.getMaxHp() / (100 / this.healPercent)) * stack, 1),
i18next.t("modifier:enemyTurnHealApply", {
pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon),
}),
true,
false,
false,
false,
true,
);
return true;
}
return false;
}
}
export class EnemyAttackStatusEffectChanceTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_ATTACK_STATUS_CHANCE];
public effect: StatusEffect;
constructor(type: TrainerItemId, effect: StatusEffect, stackCount?: number) {
super(type, stackCount);
this.effect = effect;
}
get iconName(): string {
if (this.effect === StatusEffect.POISON) {
return "wl_antidote";
}
if (this.effect === StatusEffect.PARALYSIS) {
return "wl_paralyze_heal";
}
if (this.effect === StatusEffect.BURN) {
return "wl_burn_heal";
}
return "";
}
get description(): string {
return i18next.t("modifierType:ModifierType.EnemyAttackStatusEffectChanceModifierType.description", {
chancePercent: this.getChance() * 100,
statusEffect: getStatusEffectDescriptor(this.effect),
});
}
apply(manager: TrainerItemManager, params: POKEMON_PARAMS): boolean {
const stack = manager.getStack(this.type);
const enemyPokemon = params.pokemon;
const chance = this.getChance();
if (randSeedFloat() <= chance * stack) {
return enemyPokemon.trySetStatus(this.effect, true);
}
return false;
}
getChance(): number {
return 0.025 * (this.effect === StatusEffect.BURN || this.effect === StatusEffect.POISON ? 2 : 1);
}
}
export class EnemyStatusEffectHealChanceTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_STATUS_HEAL_CHANCE];
public chance = 0.025;
get iconName(): string {
return "wl_full_heal";
}
apply(manager: TrainerItemManager, params: POKEMON_PARAMS): boolean {
const stack = manager.getStack(this.type);
const enemyPokemon = params.pokemon;
if (!enemyPokemon.status || randSeedFloat() > this.chance * stack) {
return false;
}
globalScene.phaseManager.queueMessage(
getStatusEffectHealText(enemyPokemon.status.effect, getPokemonNameWithAffix(enemyPokemon)),
);
enemyPokemon.resetStatus();
enemyPokemon.updateInfo();
return true;
}
}
export class EnemyEndureChanceTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_ENDURE_CHANCE];
public chance = 2;
get iconName(): string {
return "wl_reset_urge";
}
get description(): string {
return i18next.t("modifierType:ModifierType.EnemyEndureChanceModifierType.description", {
chancePercent: this.chance,
});
}
apply(manager: TrainerItemManager, params: POKEMON_PARAMS): boolean {
const stack = manager.getStack(this.type);
const target = params.pokemon;
if (target.waveData.endured || target.randBattleSeedInt(100) >= this.chance * stack) {
return false;
}
target.addTag(BattlerTagType.ENDURE_TOKEN, 1);
target.waveData.endured = true;
return true;
}
}
export class EnemyFusionChanceTrainerItem extends TrainerItem {
public effects: TRAINER_ITEM_EFFECT[] = [TRAINER_ITEM_EFFECT.ENEMY_FUSED_CHANCE];
public chance = 0.01;
get iconName(): string {
return "wl_custom_spliced";
}
apply(manager: TrainerItemManager, params: BOOLEAN_HOLDER_PARAMS) {
const stack = manager.getStack(this.type);
const isFusion = params.booleanHolder;
if (randSeedFloat() > this.chance * stack) {
return false;
}
isFusion.value = true;
}
}

View File

@ -21,8 +21,12 @@ import { initVouchers } from "#app/system/voucher";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { timedEventManager } from "./global-event-manager"; import { timedEventManager } from "./global-event-manager";
import { initHeldItems } from "./items/all-held-items";
import { initModifierPools } from "./modifier/init-modifier-pools"; import { initModifierPools } from "./modifier/init-modifier-pools";
import { initModifierTypes } from "./modifier/modifier-type"; import { initModifierTypes } from "./modifier/modifier-type";
import { initHeldItemPools } from "./items/init-held-item-pools";
import { initTrainerItemPools } from "./items/init-trainer-item-pools";
import { initTrainerItems } from "./items/all-trainer-items";
export class LoadingScene extends SceneBase { export class LoadingScene extends SceneBase {
public static readonly KEY = "loading"; public static readonly KEY = "loading";
@ -367,6 +371,8 @@ export class LoadingScene extends SceneBase {
initModifierTypes(); initModifierTypes();
initModifierPools(); initModifierPools();
initHeldItemPools();
initTrainerItemPools();
initAchievements(); initAchievements();
initVouchers(); initVouchers();
@ -380,6 +386,8 @@ export class LoadingScene extends SceneBase {
initSpecies(); initSpecies();
initMoves(); initMoves();
initAbilities(); initAbilities();
initHeldItems();
initTrainerItems();
initChallenges(); initChallenges();
initMysteryEncounters(); initMysteryEncounters();
} }

View File

@ -1,20 +1,11 @@
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { import { modifierPool } from "#app/modifier/modifier-pools";
dailyStarterModifierPool,
enemyBuffModifierPool,
modifierPool,
trainerModifierPool,
wildModifierPool,
} from "#app/modifier/modifier-pools";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { DoubleBattleChanceBoosterModifier, SpeciesCritBoosterModifier, TurnStatusEffectModifier } from "./modifier";
import { WeightedModifierType } from "./modifier-type"; import { WeightedModifierType } from "./modifier-type";
import { ModifierTier } from "../enums/modifier-tier"; import { RewardTier } from "#app/enums/reward-tier";
import type { WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types"; import type { WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { BerryModifier } from "./modifier";
import { BerryType } from "#enums/berry-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
@ -26,41 +17,17 @@ import { AbilityId } from "#enums/ability-id";
import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball";
// biome-ignore lint/correctness/noUnusedImports: This is used in a tsdoc comment // biome-ignore lint/correctness/noUnusedImports: This is used in a tsdoc comment
import type { initModifierTypes } from "./modifier-type"; import type { initModifierTypes } from "./modifier-type";
import { HeldItemId } from "#enums/held-item-id";
/** import { allHeldItems } from "#app/data/data-lists";
* Initialize the wild modifier pool import { TrainerItemId } from "#enums/trainer-item-id";
*/ import { allTrainerItems } from "#app/data/data-lists";
function initWildModifierPool() { import type { TurnEndStatusHeldItem } from "#app/items/held-items/turn-end-status";
wildModifierPool[ModifierTier.COMMON] = [new WeightedModifierType(modifierTypes.BERRY, 1)].map(m => {
m.setTier(ModifierTier.COMMON);
return m;
});
wildModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1)].map(m => {
m.setTier(ModifierTier.GREAT);
return m;
});
wildModifierPool[ModifierTier.ULTRA] = [
new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10),
new WeightedModifierType(modifierTypes.WHITE_HERB, 0),
].map(m => {
m.setTier(ModifierTier.ULTRA);
return m;
});
wildModifierPool[ModifierTier.ROGUE] = [new WeightedModifierType(modifierTypes.LUCKY_EGG, 4)].map(m => {
m.setTier(ModifierTier.ROGUE);
return m;
});
wildModifierPool[ModifierTier.MASTER] = [new WeightedModifierType(modifierTypes.GOLDEN_EGG, 1)].map(m => {
m.setTier(ModifierTier.MASTER);
return m;
});
}
/** /**
* Initialize the common modifier pool * Initialize the common modifier pool
*/ */
function initCommonModifierPool() { function initCommonModifierPool() {
modifierPool[ModifierTier.COMMON] = [ modifierPool[RewardTier.COMMON] = [
new WeightedModifierType(modifierTypes.POKEBALL, () => (hasMaximumBalls(PokeballType.POKEBALL) ? 0 : 6), 6), new WeightedModifierType(modifierTypes.POKEBALL, () => (hasMaximumBalls(PokeballType.POKEBALL) ? 0 : 6), 6),
new WeightedModifierType(modifierTypes.RARE_CANDY, 2), new WeightedModifierType(modifierTypes.RARE_CANDY, 2),
new WeightedModifierType( new WeightedModifierType(
@ -92,7 +59,7 @@ function initCommonModifierPool() {
party.filter( party.filter(
p => p =>
p.hp && p.hp &&
!p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) &&
p p
.getMoveset() .getMoveset()
.filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2))
@ -111,7 +78,7 @@ function initCommonModifierPool() {
party.filter( party.filter(
p => p =>
p.hp && p.hp &&
!p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) &&
p p
.getMoveset() .getMoveset()
.filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2))
@ -123,12 +90,12 @@ function initCommonModifierPool() {
}, },
3, 3,
), ),
new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(TrainerItemId.LURE, 2)),
new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4), new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4),
new WeightedModifierType(modifierTypes.BERRY, 2), new WeightedModifierType(modifierTypes.BERRY, 2),
new WeightedModifierType(modifierTypes.TM_COMMON, 2), new WeightedModifierType(modifierTypes.TM_COMMON, 2),
].map(m => { ].map(m => {
m.setTier(ModifierTier.COMMON); m.setTier(RewardTier.COMMON);
return m; return m;
}); });
} }
@ -137,7 +104,7 @@ function initCommonModifierPool() {
* Initialize the Great modifier pool * Initialize the Great modifier pool
*/ */
function initGreatModifierPool() { function initGreatModifierPool() {
modifierPool[ModifierTier.GREAT] = [ modifierPool[RewardTier.GREAT] = [
new WeightedModifierType(modifierTypes.GREAT_BALL, () => (hasMaximumBalls(PokeballType.GREAT_BALL) ? 0 : 6), 6), new WeightedModifierType(modifierTypes.GREAT_BALL, () => (hasMaximumBalls(PokeballType.GREAT_BALL) ? 0 : 6), 6),
new WeightedModifierType(modifierTypes.PP_UP, 2), new WeightedModifierType(modifierTypes.PP_UP, 2),
new WeightedModifierType( new WeightedModifierType(
@ -148,12 +115,10 @@ function initGreatModifierPool() {
p => p =>
p.hp && p.hp &&
!!p.status && !!p.status &&
!p.getHeldItems().some(i => { !p
if (i instanceof TurnStatusEffectModifier) { .getHeldItems()
return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; .filter(i => i in [HeldItemId.TOXIC_ORB, HeldItemId.FLAME_ORB])
} .some(i => (allHeldItems[i] as TurnEndStatusHeldItem).effect === p.status?.effect),
return false;
}),
).length, ).length,
3, 3,
); );
@ -214,12 +179,10 @@ function initGreatModifierPool() {
p => p =>
p.hp && p.hp &&
!!p.status && !!p.status &&
!p.getHeldItems().some(i => { !p
if (i instanceof TurnStatusEffectModifier) { .getHeldItems()
return (i as TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; .filter(i => i in [HeldItemId.TOXIC_ORB, HeldItemId.FLAME_ORB])
} .some(i => (allHeldItems[i] as TurnEndStatusHeldItem).effect === p.status?.effect),
return false;
}),
).length, ).length,
3, 3,
); );
@ -239,7 +202,7 @@ function initGreatModifierPool() {
party.filter( party.filter(
p => p =>
p.hp && p.hp &&
!p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) &&
p p
.getMoveset() .getMoveset()
.filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2))
@ -258,7 +221,7 @@ function initGreatModifierPool() {
party.filter( party.filter(
p => p =>
p.hp && p.hp &&
!p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) && !p.heldItemManager.hasItem(HeldItemId.LEPPA_BERRY) &&
p p
.getMoveset() .getMoveset()
.filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)) .filter(m => m.ppUsed && m.getMovePp() - m.ppUsed <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2))
@ -271,7 +234,7 @@ function initGreatModifierPool() {
3, 3,
), ),
new WeightedModifierType(modifierTypes.DIRE_HIT, 4), new WeightedModifierType(modifierTypes.DIRE_HIT, 4),
new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(15, 4)), new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(TrainerItemId.SUPER_LURE, 4)),
new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)), new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)),
new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 4), new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 4),
new WeightedModifierType( new WeightedModifierType(
@ -331,7 +294,7 @@ function initGreatModifierPool() {
1, 1,
), ),
].map(m => { ].map(m => {
m.setTier(ModifierTier.GREAT); m.setTier(RewardTier.GREAT);
return m; return m;
}); });
} }
@ -340,9 +303,9 @@ function initGreatModifierPool() {
* Initialize the Ultra modifier pool * Initialize the Ultra modifier pool
*/ */
function initUltraModifierPool() { function initUltraModifierPool() {
modifierPool[ModifierTier.ULTRA] = [ modifierPool[RewardTier.ULTRA] = [
new WeightedModifierType(modifierTypes.ULTRA_BALL, () => (hasMaximumBalls(PokeballType.ULTRA_BALL) ? 0 : 15), 15), new WeightedModifierType(modifierTypes.ULTRA_BALL, () => (hasMaximumBalls(PokeballType.ULTRA_BALL) ? 0 : 15), 15),
new WeightedModifierType(modifierTypes.MAX_LURE, lureWeightFunc(30, 4)), new WeightedModifierType(modifierTypes.MAX_LURE, lureWeightFunc(TrainerItemId.MAX_LURE, 4)),
new WeightedModifierType(modifierTypes.BIG_NUGGET, skipInLastClassicWaveOrDefault(12)), new WeightedModifierType(modifierTypes.BIG_NUGGET, skipInLastClassicWaveOrDefault(12)),
new WeightedModifierType(modifierTypes.PP_MAX, 3), new WeightedModifierType(modifierTypes.PP_MAX, 3),
new WeightedModifierType(modifierTypes.MINT, 4), new WeightedModifierType(modifierTypes.MINT, 4),
@ -368,7 +331,7 @@ function initUltraModifierPool() {
(p.isFusion() && p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)) (p.isFusion() && p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions))
) { ) {
// Check if Pokemon is already holding an Eviolite // Check if Pokemon is already holding an Eviolite
return !p.getHeldItems().some(i => i.type.id === "EVIOLITE"); return !p.heldItemManager.hasItem(HeldItemId.EVIOLITE);
} }
return false; return false;
}) })
@ -385,7 +348,7 @@ function initUltraModifierPool() {
// If a party member doesn't already have a Leek and is one of the relevant species, Leek can appear // If a party member doesn't already have a Leek and is one of the relevant species, Leek can appear
return party.some( return party.some(
p => p =>
!p.getHeldItems().some(i => i instanceof SpeciesCritBoosterModifier) && !p.heldItemManager.hasItem(HeldItemId.LEEK) &&
(checkedSpecies.includes(p.getSpeciesForm(true).speciesId) || (checkedSpecies.includes(p.getSpeciesForm(true).speciesId) ||
(p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId))), (p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId))),
) )
@ -398,7 +361,7 @@ function initUltraModifierPool() {
modifierTypes.TOXIC_ORB, modifierTypes.TOXIC_ORB,
(party: Pokemon[]) => { (party: Pokemon[]) => {
return party.some(p => { return party.some(p => {
const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); const isHoldingOrb = p.getHeldItems().some(i => i in [HeldItemId.FLAME_ORB, HeldItemId.TOXIC_ORB]);
if (!isHoldingOrb) { if (!isHoldingOrb) {
const moveset = p const moveset = p
@ -444,7 +407,7 @@ function initUltraModifierPool() {
modifierTypes.FLAME_ORB, modifierTypes.FLAME_ORB,
(party: Pokemon[]) => { (party: Pokemon[]) => {
return party.some(p => { return party.some(p => {
const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); const isHoldingOrb = p.getHeldItems().some(i => i in [HeldItemId.FLAME_ORB, HeldItemId.TOXIC_ORB]);
if (!isHoldingOrb) { if (!isHoldingOrb) {
const moveset = p const moveset = p
@ -490,13 +453,8 @@ function initUltraModifierPool() {
modifierTypes.MYSTICAL_ROCK, modifierTypes.MYSTICAL_ROCK,
(party: Pokemon[]) => { (party: Pokemon[]) => {
return party.some(p => { return party.some(p => {
let isHoldingMax = false; const stack = p.heldItemManager.getStack(HeldItemId.MYSTICAL_ROCK);
for (const i of p.getHeldItems()) { const isHoldingMax = stack === allHeldItems[HeldItemId.MYSTICAL_ROCK].maxStackCount;
if (i.type.id === "MYSTICAL_ROCK") {
isHoldingMax = i.getStackCount() === i.getMaxStackCount();
break;
}
}
if (!isHoldingMax) { if (!isHoldingMax) {
const moveset = p.getMoveset(true).map(m => m.moveId); const moveset = p.getMoveset(true).map(m => m.moveId);
@ -558,13 +516,13 @@ function initUltraModifierPool() {
new WeightedModifierType(modifierTypes.QUICK_CLAW, 3), new WeightedModifierType(modifierTypes.QUICK_CLAW, 3),
new WeightedModifierType(modifierTypes.WIDE_LENS, 7), new WeightedModifierType(modifierTypes.WIDE_LENS, 7),
].map(m => { ].map(m => {
m.setTier(ModifierTier.ULTRA); m.setTier(RewardTier.ULTRA);
return m; return m;
}); });
} }
function initRogueModifierPool() { function initRogueModifierPool() {
modifierPool[ModifierTier.ROGUE] = [ modifierPool[RewardTier.ROGUE] = [
new WeightedModifierType(modifierTypes.ROGUE_BALL, () => (hasMaximumBalls(PokeballType.ROGUE_BALL) ? 0 : 16), 16), new WeightedModifierType(modifierTypes.ROGUE_BALL, () => (hasMaximumBalls(PokeballType.ROGUE_BALL) ? 0 : 16), 16),
new WeightedModifierType(modifierTypes.RELIC_GOLD, skipInLastClassicWaveOrDefault(2)), new WeightedModifierType(modifierTypes.RELIC_GOLD, skipInLastClassicWaveOrDefault(2)),
new WeightedModifierType(modifierTypes.LEFTOVERS, 3), new WeightedModifierType(modifierTypes.LEFTOVERS, 3),
@ -602,7 +560,7 @@ function initRogueModifierPool() {
3, 3,
), ),
].map(m => { ].map(m => {
m.setTier(ModifierTier.ROGUE); m.setTier(RewardTier.ROGUE);
return m; return m;
}); });
} }
@ -611,7 +569,7 @@ function initRogueModifierPool() {
* Initialize the Master modifier pool * Initialize the Master modifier pool
*/ */
function initMasterModifierPool() { function initMasterModifierPool() {
modifierPool[ModifierTier.MASTER] = [ modifierPool[RewardTier.MASTER] = [
new WeightedModifierType(modifierTypes.MASTER_BALL, () => (hasMaximumBalls(PokeballType.MASTER_BALL) ? 0 : 24), 24), new WeightedModifierType(modifierTypes.MASTER_BALL, () => (hasMaximumBalls(PokeballType.MASTER_BALL) ? 0 : 24), 24),
new WeightedModifierType(modifierTypes.SHINY_CHARM, 14), new WeightedModifierType(modifierTypes.SHINY_CHARM, 14),
new WeightedModifierType(modifierTypes.HEALING_CHARM, 18), new WeightedModifierType(modifierTypes.HEALING_CHARM, 18),
@ -644,140 +602,7 @@ function initMasterModifierPool() {
1, 1,
), ),
].map(m => { ].map(m => {
m.setTier(ModifierTier.MASTER); m.setTier(RewardTier.MASTER);
return m;
});
}
function initTrainerModifierPool() {
trainerModifierPool[ModifierTier.COMMON] = [
new WeightedModifierType(modifierTypes.BERRY, 8),
new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3),
].map(m => {
m.setTier(ModifierTier.COMMON);
return m;
});
trainerModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3)].map(m => {
m.setTier(ModifierTier.GREAT);
return m;
});
trainerModifierPool[ModifierTier.ULTRA] = [
new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10),
new WeightedModifierType(modifierTypes.WHITE_HERB, 0),
].map(m => {
m.setTier(ModifierTier.ULTRA);
return m;
});
trainerModifierPool[ModifierTier.ROGUE] = [
new WeightedModifierType(modifierTypes.FOCUS_BAND, 2),
new WeightedModifierType(modifierTypes.LUCKY_EGG, 4),
new WeightedModifierType(modifierTypes.QUICK_CLAW, 1),
new WeightedModifierType(modifierTypes.GRIP_CLAW, 1),
new WeightedModifierType(modifierTypes.WIDE_LENS, 1),
].map(m => {
m.setTier(ModifierTier.ROGUE);
return m;
});
trainerModifierPool[ModifierTier.MASTER] = [
new WeightedModifierType(modifierTypes.KINGS_ROCK, 1),
new WeightedModifierType(modifierTypes.LEFTOVERS, 1),
new WeightedModifierType(modifierTypes.SHELL_BELL, 1),
new WeightedModifierType(modifierTypes.SCOPE_LENS, 1),
].map(m => {
m.setTier(ModifierTier.MASTER);
return m;
});
}
/**
* Initialize the enemy buff modifier pool
*/
function initEnemyBuffModifierPool() {
enemyBuffModifierPool[ModifierTier.COMMON] = [
new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 9),
new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 9),
new WeightedModifierType(modifierTypes.ENEMY_ATTACK_POISON_CHANCE, 3),
new WeightedModifierType(modifierTypes.ENEMY_ATTACK_PARALYZE_CHANCE, 3),
new WeightedModifierType(modifierTypes.ENEMY_ATTACK_BURN_CHANCE, 3),
new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 9),
new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 4),
new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1),
].map(m => {
m.setTier(ModifierTier.COMMON);
return m;
});
enemyBuffModifierPool[ModifierTier.GREAT] = [
new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 5),
new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 5),
new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 5),
new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 5),
new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 1),
].map(m => {
m.setTier(ModifierTier.GREAT);
return m;
});
enemyBuffModifierPool[ModifierTier.ULTRA] = [
new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_BOOSTER, 10),
new WeightedModifierType(modifierTypes.ENEMY_DAMAGE_REDUCTION, 10),
new WeightedModifierType(modifierTypes.ENEMY_HEAL, 10),
new WeightedModifierType(modifierTypes.ENEMY_STATUS_EFFECT_HEAL_CHANCE, 10),
new WeightedModifierType(modifierTypes.ENEMY_ENDURE_CHANCE, 10),
new WeightedModifierType(modifierTypes.ENEMY_FUSED_CHANCE, 5),
].map(m => {
m.setTier(ModifierTier.ULTRA);
return m;
});
enemyBuffModifierPool[ModifierTier.ROGUE] = [].map((m: WeightedModifierType) => {
m.setTier(ModifierTier.ROGUE);
return m;
});
enemyBuffModifierPool[ModifierTier.MASTER] = [].map((m: WeightedModifierType) => {
m.setTier(ModifierTier.MASTER);
return m;
});
}
/**
* Initialize the daily starter modifier pool
*/
function initDailyStarterModifierPool() {
dailyStarterModifierPool[ModifierTier.COMMON] = [
new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 1),
new WeightedModifierType(modifierTypes.BERRY, 3),
].map(m => {
m.setTier(ModifierTier.COMMON);
return m;
});
dailyStarterModifierPool[ModifierTier.GREAT] = [new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 5)].map(
m => {
m.setTier(ModifierTier.GREAT);
return m;
},
);
dailyStarterModifierPool[ModifierTier.ULTRA] = [
new WeightedModifierType(modifierTypes.REVIVER_SEED, 4),
new WeightedModifierType(modifierTypes.SOOTHE_BELL, 1),
new WeightedModifierType(modifierTypes.SOUL_DEW, 1),
new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, 1),
].map(m => {
m.setTier(ModifierTier.ULTRA);
return m;
});
dailyStarterModifierPool[ModifierTier.ROGUE] = [
new WeightedModifierType(modifierTypes.GRIP_CLAW, 5),
new WeightedModifierType(modifierTypes.BATON, 2),
new WeightedModifierType(modifierTypes.FOCUS_BAND, 5),
new WeightedModifierType(modifierTypes.QUICK_CLAW, 3),
new WeightedModifierType(modifierTypes.KINGS_ROCK, 3),
].map(m => {
m.setTier(ModifierTier.ROGUE);
return m;
});
dailyStarterModifierPool[ModifierTier.MASTER] = [
new WeightedModifierType(modifierTypes.LEFTOVERS, 1),
new WeightedModifierType(modifierTypes.SHELL_BELL, 1),
].map(m => {
m.setTier(ModifierTier.MASTER);
return m; return m;
}); });
} }
@ -793,12 +618,6 @@ export function initModifierPools() {
initUltraModifierPool(); initUltraModifierPool();
initRogueModifierPool(); initRogueModifierPool();
initMasterModifierPool(); initMasterModifierPool();
// Modifier pools for specific scenarios
initWildModifierPool();
initTrainerModifierPool();
initEnemyBuffModifierPool();
initDailyStarterModifierPool();
} }
/** /**
@ -829,16 +648,15 @@ function skipInLastClassicWaveOrDefault(defaultWeight: number): WeightedModifier
/** /**
* High order function that returns a WeightedModifierTypeWeightFunc to ensure Lures don't spawn on Classic 199 * 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 * or if the lure still has over 60% of its duration left
* @param maxBattles The max battles the lure type in question lasts. 10 for green, 15 for Super, 30 for Max * @param lureId The id of the lure type in question.
* @param weight The desired weight for the lure when it does spawn * @param weight The desired weight for the lure when it does spawn
* @returns A WeightedModifierTypeWeightFunc * @returns A WeightedModifierTypeWeightFunc
*/ */
function lureWeightFunc(maxBattles: number, weight: number): WeightedModifierTypeWeightFunc { function lureWeightFunc(lureId: TrainerItemId, weight: number): WeightedModifierTypeWeightFunc {
return () => { return () => {
const lures = globalScene.getModifiers(DoubleBattleChanceBoosterModifier); const lureCount = globalScene.trainerItems.getStack(lureId);
return !(globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex === 199) && return !(globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex === 199) &&
(lures.length === 0 || lureCount < allTrainerItems[lureId].getMaxStackCount() * 0.6
lures.filter(m => m.getMaxBattles() === maxBattles && m.getBattleCount() >= maxBattles * 0.6).length === 0)
? weight ? weight
: 0; : 0;
}; };

View File

@ -6,11 +6,3 @@
import type { ModifierPool } from "#app/@types/modifier-types"; import type { ModifierPool } from "#app/@types/modifier-types";
export const modifierPool: ModifierPool = {}; export const modifierPool: ModifierPool = {};
export const wildModifierPool: ModifierPool = {};
export const trainerModifierPool: ModifierPool = {};
export const enemyBuffModifierPool: ModifierPool = {};
export const dailyStarterModifierPool: ModifierPool = {};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,8 @@ import { TrainerType } from "#enums/trainer-type";
import { Unlockables } from "#enums/unlockables"; import { Unlockables } from "#enums/unlockables";
import { VariantTier } from "#enums/variant-tier"; import { VariantTier } from "#enums/variant-tier";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { HeldItemConfiguration } from "./items/held-item-data-types";
import { TrainerItemConfiguration } from "./items/trainer-item-data-types";
/** /**
* This comment block exists to prevent IDEs from automatically removing unused imports * This comment block exists to prevent IDEs from automatically removing unused imports
@ -271,18 +273,15 @@ class DefaultOverrides {
* STARTING_HELD_ITEM_OVERRIDE = [{name: "BERRY"}] * STARTING_HELD_ITEM_OVERRIDE = [{name: "BERRY"}]
* ``` * ```
*/ */
readonly STARTING_MODIFIER_OVERRIDE: ModifierOverride[] = []; /** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */
/** readonly STARTING_TRAINER_ITEMS_OVERRIDE: TrainerItemConfiguration = [];
* Override array of {@linkcode ModifierOverride}s used to provide modifiers to enemies. /** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */
* readonly OPP_TRAINER_ITEMS_OVERRIDE: TrainerItemConfiguration = [];
* Note that any previous modifiers are cleared.
*/
readonly OPP_MODIFIER_OVERRIDE: ModifierOverride[] = [];
/** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */ /** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */
readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; readonly STARTING_HELD_ITEMS_OVERRIDE: HeldItemConfiguration = [];
/** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */ /** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */
readonly OPP_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; readonly OPP_HELD_ITEMS_OVERRIDE: HeldItemConfiguration = [];
/** /**
* Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave. * Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave.

View File

@ -1,9 +1,7 @@
import { ModifierTier } from "#enums/modifier-tier"; import { RewardTier } from "#enums/reward-tier";
import { regenerateModifierPoolThresholds, getEnemyBuffModifierForWave } from "#app/modifier/modifier-type";
import { ModifierPoolType } from "#enums/modifier-pool-type";
import { EnemyPersistentModifier } from "#app/modifier/modifier";
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { assignEnemyBuffTokenForWave } from "#app/items/trainer-item-pool";
export class AddEnemyBuffModifierPhase extends Phase { export class AddEnemyBuffModifierPhase extends Phase {
public readonly phaseName = "AddEnemyBuffModifierPhase"; public readonly phaseName = "AddEnemyBuffModifierPhase";
@ -11,26 +9,13 @@ export class AddEnemyBuffModifierPhase extends Phase {
super.start(); super.start();
const waveIndex = globalScene.currentBattle.waveIndex; const waveIndex = globalScene.currentBattle.waveIndex;
const tier = !(waveIndex % 1000) const tier = !(waveIndex % 1000) ? RewardTier.ULTRA : !(waveIndex % 250) ? RewardTier.GREAT : RewardTier.COMMON;
? ModifierTier.ULTRA
: !(waveIndex % 250)
? ModifierTier.GREAT
: ModifierTier.COMMON;
regenerateModifierPoolThresholds(globalScene.getEnemyParty(), ModifierPoolType.ENEMY_BUFF);
const count = Math.ceil(waveIndex / 250); const count = Math.ceil(waveIndex / 250);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
globalScene.addEnemyModifier( assignEnemyBuffTokenForWave(tier);
getEnemyBuffModifierForWave(
tier,
globalScene.findModifiers(m => m instanceof EnemyPersistentModifier, false),
),
true,
true,
);
} }
globalScene.updateModifiers(false, true); globalScene.updateItems(false);
this.end(); this.end();
} }
} }

View File

@ -12,7 +12,6 @@ import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
import type { EnemyPokemon } from "#app/field/pokemon"; import type { EnemyPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { PokemonPhase } from "#app/phases/pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import type { PartyOption } from "#app/ui/party-ui-handler"; import type { PartyOption } from "#app/ui/party-ui-handler";
@ -255,6 +254,7 @@ export class AttemptCapturePhase extends PokemonPhase {
}), }),
null, null,
() => { () => {
const heldItemConfig = pokemon.heldItemManager.generateHeldItemConfiguration();
const end = () => { const end = () => {
globalScene.phaseManager.unshiftNew("VictoryPhase", this.battlerIndex); globalScene.phaseManager.unshiftNew("VictoryPhase", this.battlerIndex);
globalScene.pokemonInfoContainer.hide(); globalScene.pokemonInfoContainer.hide();
@ -265,24 +265,20 @@ export class AttemptCapturePhase extends PokemonPhase {
globalScene.addFaintedEnemyScore(pokemon); globalScene.addFaintedEnemyScore(pokemon);
pokemon.hp = 0; pokemon.hp = 0;
pokemon.trySetStatus(StatusEffect.FAINT); pokemon.trySetStatus(StatusEffect.FAINT);
globalScene.clearEnemyHeldItemModifiers();
pokemon.leaveField(true, true, true); pokemon.leaveField(true, true, true);
}; };
const addToParty = (slotIndex?: number) => { const addToParty = (slotIndex?: number) => {
const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex); const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex);
const modifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) { if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) {
globalScene.validateAchv(achvs.SHINY_PARTY); globalScene.validateAchv(achvs.SHINY_PARTY);
} }
Promise.all(modifiers.map(m => globalScene.addModifier(m, true))).then(() => { globalScene.updateItems(true);
globalScene.updateModifiers(true); removePokemon();
removePokemon(); if (newPokemon) {
if (newPokemon) { newPokemon.loadAssets().then(end);
newPokemon.loadAssets().then(end); } else {
} else { end();
end(); }
}
});
}; };
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) {
@ -307,6 +303,7 @@ export class AttemptCapturePhase extends PokemonPhase {
pokemon.variant, pokemon.variant,
pokemon.ivs, pokemon.ivs,
pokemon.nature, pokemon.nature,
heldItemConfig,
pokemon, pokemon,
); );
globalScene.ui.setMode( globalScene.ui.setMode(

View File

@ -40,8 +40,6 @@ export class AttemptRunPhase extends FieldPhase {
onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy()), onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy()),
}); });
globalScene.clearEnemyHeldItemModifiers();
enemyField.forEach(enemyPokemon => { enemyField.forEach(enemyPokemon => {
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy()); enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
enemyPokemon.hp = 0; enemyPokemon.hp = 0;

View File

@ -1,6 +1,5 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier";
import { BattlePhase } from "./battle-phase"; import { BattlePhase } from "./battle-phase";
export class BattleEndPhase extends BattlePhase { export class BattleEndPhase extends BattlePhase {
@ -72,7 +71,6 @@ export class BattleEndPhase extends BattlePhase {
globalScene.currentBattle.pickUpScatteredMoney(); globalScene.currentBattle.pickUpScatteredMoney();
} }
globalScene.clearEnemyHeldItemModifiers();
for (const p of globalScene.getEnemyParty()) { for (const p of globalScene.getEnemyParty()) {
try { try {
p.destroy(); p.destroy();
@ -81,20 +79,9 @@ export class BattleEndPhase extends BattlePhase {
} }
} }
const lapsingModifiers = globalScene.findModifiers( globalScene.trainerItems.lapseItems();
m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier,
) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[];
for (const m of lapsingModifiers) {
const args: any[] = [];
if (m instanceof LapsingPokemonHeldItemModifier) {
args.push(globalScene.getPokemonById(m.pokemonId));
}
if (!m.lapse(...args)) {
globalScene.removeModifier(m);
}
}
globalScene.updateModifiers(); globalScene.updateItems();
this.end(); this.end();
} }
} }

Some files were not shown because too many files have changed in this diff Show More