diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index c080122f922..32d01817ff8 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -42,8 +42,6 @@ import type { AttackTypeBoosterModifierType, ModifierTypeOption } from "#app/mod import { modifierTypes } from "#app/modifier/modifier-type"; import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { - AttackTypeBoosterModifier, - BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, GigantamaxAccessModifier, MegaEvolutionAccessModifier, diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 8db0d5fc0e8..322aab4c6b5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -92,11 +92,8 @@ import { PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, - SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, - StatBoosterModifier, - CritBoosterModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, @@ -257,6 +254,8 @@ import { timedEventManager } from "#app/global-event-manager"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; import { ResetStatusPhase } from "#app/phases/reset-status-phase"; import { PokemonItemManager } from "./pokemon-held-item-manager"; +import { applyHeldItems } from "#app/items/all-held-items"; +import { ITEM_EFFECT } from "#app/items/held-item"; export enum LearnMoveSituation { MISC, @@ -1446,7 +1445,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getCritStage(source: Pokemon, move: Move): number { const critStage = new NumberHolder(0); applyMoveAttrs(HighCritAttr, source, this, move, critStage); - globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage); + applyHeldItems(ITEM_EFFECT.CRIT_BOOST, { pokemon: source, critStage: critStage }); globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); applyAbAttrs(BonusCritAbAttr, source, null, false, critStage); const critBoostTag = source.getTag(CritBoostTag); @@ -1503,7 +1502,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ): number { const statValue = new NumberHolder(this.getStat(stat, false)); if (!ignoreHeldItems) { - globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); + applyHeldItems(ITEM_EFFECT.STAT_BOOST, { pokemon: this, stat: stat, statValue: statValue }); } // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway @@ -3995,7 +3994,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { surviveDamage.value = this.lapseTag(BattlerTagType.ENDURE_TOKEN); } if (!surviveDamage.value) { - globalScene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage); + applyHeldItems(ITEM_EFFECT.SURVIVE_CHANCE, { pokemon: this, surviveDamage: surviveDamage }); } if (surviveDamage.value) { damage = this.hp - 1; diff --git a/src/items/all-held-items.ts b/src/items/all-held-items.ts index a016f4880cd..9c3f7c91473 100644 --- a/src/items/all-held-items.ts +++ b/src/items/all-held-items.ts @@ -4,6 +4,7 @@ import { HeldItemId } from "#enums/held-item-id"; import type { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { Stat, type PermanentStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import { ITEM_EFFECT } from "./held-item"; import { type ATTACK_TYPE_BOOST_PARAMS, @@ -16,7 +17,10 @@ import { permanentStatToHeldItem, } from "./held-items/base-stat-booster"; 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 EXP_BOOST_PARAMS, ExpBoosterHeldItem } from "./held-items/exp-booster"; +import { type FLINCH_CHANCE_PARAMS, FlinchChanceHeldItem } from "./held-items/flinch-chance"; import { type HIT_HEAL_PARAMS, HitHealHeldItem } from "./held-items/hit-heal"; import { InstantReviveHeldItem, type INSTANT_REVIVE_PARAMS } from "./held-items/instant-revive"; import { @@ -28,8 +32,9 @@ import { SpeciesStatBoostHeldItem, type STAT_BOOST_PARAMS, } from "./held-items/stat-booster"; -import type { TURN_END_HEAL_PARAMS } from "./held-items/turn-end-heal"; -import { TurnEndHealHeldItem } from "./held-items/turn-end-heal"; +import { type SURVIVE_CHANCE_PARAMS, SurviveChanceHeldItem } from "./held-items/survive-chance"; +import { type TURN_END_HEAL_PARAMS, TurnEndHealHeldItem } from "./held-items/turn-end-heal"; +import { type TURN_END_STATUS_PARAMS, TurnEndStatusHeldItem } from "./held-items/turn-end-status"; export const allHeldItems = {}; @@ -55,6 +60,7 @@ export function initHeldItems() { allHeldItems[heldItemType] = new AttackTypeBoosterHeldItem(heldItemType, 99, pokemonType, 0.2); } + // Items that boost specific stats allHeldItems[HeldItemId.EVIOLITE] = new EvolutionStatBoostHeldItem( HeldItemId.EVIOLITE, 1, @@ -86,12 +92,27 @@ export function initHeldItems() { 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.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.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; @@ -109,6 +130,11 @@ type APPLY_HELD_ITEMS_PARAMS = { [ITEM_EFFECT.BASE_STAT_BOOSTER]: BASE_STAT_BOOSTER_PARAMS; [ITEM_EFFECT.INSTANT_REVIVE]: INSTANT_REVIVE_PARAMS; [ITEM_EFFECT.STAT_BOOST]: STAT_BOOST_PARAMS; + [ITEM_EFFECT.CRIT_BOOST]: CRIT_BOOST_PARAMS; + [ITEM_EFFECT.TURN_END_STATUS]: TURN_END_STATUS_PARAMS; + [ITEM_EFFECT.SURVIVE_CHANCE]: SURVIVE_CHANCE_PARAMS; + [ITEM_EFFECT.BYPASS_SPEED_CHANCE]: BYPASS_SPEED_CHANCE_PARAMS; + [ITEM_EFFECT.FLINCH_CHANCE]: FLINCH_CHANCE_PARAMS; }; export function applyHeldItems(effect: T, params: APPLY_HELD_ITEMS_PARAMS[T]) { diff --git a/src/items/held-item.ts b/src/items/held-item.ts index f8cbfcec5eb..f8fd3fb592a 100644 --- a/src/items/held-item.ts +++ b/src/items/held-item.ts @@ -15,6 +15,11 @@ export const ITEM_EFFECT = { 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, } as const; export type ITEM_EFFECT = (typeof ITEM_EFFECT)[keyof typeof ITEM_EFFECT]; diff --git a/src/items/held-items/bypass-speed-chance.ts b/src/items/held-items/bypass-speed-chance.ts new file mode 100644 index 00000000000..388e716bc30 --- /dev/null +++ b/src/items/held-items/bypass-speed-chance.ts @@ -0,0 +1,62 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, ITEM_EFFECT } from "#app/items/held-item"; +import type { BooleanHolder } from "#app/utils/common"; +import { globalScene } from "#app/global-scene"; +import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { Command } from "#app/ui/command-ui-handler"; + +export interface BYPASS_SPEED_CHANCE_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + doBypassSpeed: BooleanHolder; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class BypassSpeedChanceHeldItem extends HeldItem { + public effects: ITEM_EFFECT[] = [ITEM_EFFECT.BYPASS_SPEED_CHANCE]; + + /** + * Checks if {@linkcode BypassSpeedChanceModifier} should be applied + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed + * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { + // return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; + // } + + /** + * Applies {@linkcode BypassSpeedChanceModifier} + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed + * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied + */ + apply(params: BYPASS_SPEED_CHANCE_PARAMS): boolean { + const pokemon = params.pokemon; + const doBypassSpeed = params.doBypassSpeed; + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < stackCount) { + doBypassSpeed.value = true; + const isCommandFight = + globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; + + if (isCommandFight) { + globalScene.queueMessage( + i18next.t("modifier:bypassSpeedChanceApply", { + pokemonName: getPokemonNameWithAffix(pokemon), + itemName: this.name, + }), + ); + } + return true; + } + + return false; + } +} diff --git a/src/items/held-items/crit-booster.ts b/src/items/held-items/crit-booster.ts new file mode 100644 index 00000000000..90502042844 --- /dev/null +++ b/src/items/held-items/crit-booster.ts @@ -0,0 +1,86 @@ +import type Pokemon from "#app/field/pokemon"; +import type { NumberHolder } from "#app/utils/common"; +import type { HeldItemId } from "#enums/held-item-id"; +import type { SpeciesId } from "#enums/species-id"; +import { HeldItem, ITEM_EFFECT } from "../held-item"; + +export interface CRIT_BOOST_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + critStage: NumberHolder; +} + +/** + * Modifier used for held items that apply critical-hit stage boost(s). + * using a multiplier. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class CritBoostHeldItem extends HeldItem { + public effects: ITEM_EFFECT[] = [ITEM_EFFECT.CRIT_BOOST]; + + /** The amount of stages by which the held item increases the current critical-hit stage value */ + protected stageIncrement: number; + + constructor(type: HeldItemId, maxStackCount = 1, stageIncrement: number) { + super(type, maxStackCount); + + this.stageIncrement = stageIncrement; + } + + /** + * Increases the current critical-hit stage value by {@linkcode stageIncrement}. + * @param _pokemon {@linkcode Pokemon} N/A + * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level + * @returns always `true` + */ + apply(params: CRIT_BOOST_PARAMS): boolean { + params.critStage.value += this.stageIncrement; + return true; + } +} + +/** + * Modifier used for held items that apply critical-hit stage boost(s) + * if the holder is of a specific {@linkcode SpeciesId}. + * @extends CritBoosterModifier + * @see {@linkcode shouldApply} + */ +export class SpeciesCritBoostHeldItem extends CritBoostHeldItem { + /** The species that the held item's critical-hit stage boost applies to */ + private species: SpeciesId[]; + + constructor(type: HeldItemId, maxStackCount = 1, stageIncrement: number, species: SpeciesId[]) { + super(type, maxStackCount, stageIncrement); + + this.species = species; + } + + /** + * Checks if the holder's {@linkcode SpeciesId} (or its fused species) is listed + * in {@linkcode species}. + * @param pokemon {@linkcode Pokemon} that holds the held item + * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level + * @returns `true` if the critical-hit level can be incremented, false otherwise + */ + // override shouldApply(pokemon: Pokemon, critStage: NumberHolder): boolean { + // return ( + // super.shouldApply(pokemon, critStage) && + // (this.species.includes(pokemon.getSpeciesForm(true).speciesId) || + // (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId))) + // ); + // } + + apply(params: CRIT_BOOST_PARAMS): boolean { + const pokemon = params.pokemon; + const fitsSpecies = + this.species.includes(pokemon.getSpeciesForm(true).speciesId) || + (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId)); + + if (fitsSpecies) { + return super.apply(params); + } + + return false; + } +} diff --git a/src/items/held-items/exp-booster.ts b/src/items/held-items/exp-booster.ts index 79ffc4ba8bc..cf1ecc2d4b0 100644 --- a/src/items/held-items/exp-booster.ts +++ b/src/items/held-items/exp-booster.ts @@ -1,7 +1,6 @@ import type Pokemon from "#app/field/pokemon"; import type { NumberHolder } from "#app/utils/common"; -import { HeldItemId } from "#enums/held-item-id"; -import i18next from "i18next"; +import type { HeldItemId } from "#enums/held-item-id"; import { HeldItem, ITEM_EFFECT } from "../held-item"; export interface EXP_BOOST_PARAMS { @@ -20,22 +19,6 @@ export class ExpBoosterHeldItem extends HeldItem { this.boostMultiplier = boostPercent * 0.01; } - get name(): string { - return this.type === HeldItemId.LUCKY_EGG - ? i18next.t("modifierType:ModifierType.LUCKY_EGG.name") - : i18next.t("modifierType:ModifierType.GOLDEN_EGG.name"); - } - - get description(): string { - return this.type === HeldItemId.LUCKY_EGG - ? i18next.t("modifierType:ModifierType.LUCKY_EGG.description") - : i18next.t("modifierType:ModifierType.GOLDEN_EGG.description"); - } - - get icon(): string { - return this.type === HeldItemId.LUCKY_EGG ? "lucky_egg" : "golden_egg"; - } - // TODO: What do we do with this? Need to look up all the shouldApply /** * Checks if {@linkcode PokemonExpBoosterModifier} should be applied diff --git a/src/items/held-items/flinch-chance.ts b/src/items/held-items/flinch-chance.ts new file mode 100644 index 00000000000..7f4a64bb507 --- /dev/null +++ b/src/items/held-items/flinch-chance.ts @@ -0,0 +1,57 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, ITEM_EFFECT } from "#app/items/held-item"; +import type { BooleanHolder } from "#app/utils/common"; +import type { HeldItemId } from "#enums/held-item-id"; + +export interface FLINCH_CHANCE_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + flinched: BooleanHolder; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class FlinchChanceHeldItem extends HeldItem { + public effects: ITEM_EFFECT[] = [ITEM_EFFECT.FLINCH_CHANCE]; + private chance: number; + + constructor(type: HeldItemId, maxStackCount = 1, chance: number) { + super(type, maxStackCount); + + this.chance = chance; // 10 + } + + /** + * Checks if {@linkcode FlinchChanceModifier} should be applied + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param flinched {@linkcode BooleanHolder} that is `true` if the pokemon flinched + * @returns `true` if {@linkcode FlinchChanceModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, flinched?: BooleanHolder): boolean { + // return super.shouldApply(pokemon, flinched) && !!flinched; + // } + + /** + * Applies {@linkcode FlinchChanceModifier} to randomly flinch targets hit. + * @param pokemon - The {@linkcode Pokemon} that holds the item + * @param flinched - A {@linkcode BooleanHolder} holding whether the pokemon has flinched + * @returns `true` if {@linkcode FlinchChanceModifier} was applied successfully + */ + apply(params: FLINCH_CHANCE_PARAMS): boolean { + const pokemon = params.pokemon; + const flinched = params.flinched; + const stackCount = pokemon.heldItemManager.getStack(this.type); + // The check for pokemon.summonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch + // TODO: Since summonData is always defined now, we can probably remove this + if (pokemon.summonData && !flinched.value && pokemon.randBattleSeedInt(100) < stackCount * this.chance) { + flinched.value = true; + return true; + } + + return false; + } +} diff --git a/src/items/held-items/survive-chance.ts b/src/items/held-items/survive-chance.ts new file mode 100644 index 00000000000..245483adf4f --- /dev/null +++ b/src/items/held-items/survive-chance.ts @@ -0,0 +1,57 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, ITEM_EFFECT } from "#app/items/held-item"; +import type { BooleanHolder } from "#app/utils/common"; +import { globalScene } from "#app/global-scene"; +import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; + +export interface SURVIVE_CHANCE_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; + surviveDamage: BooleanHolder; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class SurviveChanceHeldItem extends HeldItem { + public effects: ITEM_EFFECT[] = [ITEM_EFFECT.SURVIVE_CHANCE]; + + /** + * Checks if the {@linkcode SurviveDamageModifier} should be applied + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage + * @returns `true` if the {@linkcode SurviveDamageModifier} should be applied + */ + // override shouldApply(pokemon?: Pokemon, surviveDamage?: BooleanHolder): boolean { + // return super.shouldApply(pokemon, surviveDamage) && !!surviveDamage; + // } + + /** + * Applies {@linkcode SurviveDamageModifier} + * @param pokemon the {@linkcode Pokemon} that holds the item + * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage + * @returns `true` if the survive damage has been applied + */ + apply(params: SURVIVE_CHANCE_PARAMS): boolean { + const pokemon = params.pokemon; + const surviveDamage = params.surviveDamage; + const stackCount = pokemon.heldItemManager.getStack(this.type); + if (!surviveDamage.value && pokemon.randBattleSeedInt(10) < stackCount) { + surviveDamage.value = true; + + globalScene.queueMessage( + i18next.t("modifier:surviveDamageApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.name, + }), + ); + return true; + } + + return false; + } +} diff --git a/src/items/held-items/turn-end-status.ts b/src/items/held-items/turn-end-status.ts new file mode 100644 index 00000000000..5352be4ca63 --- /dev/null +++ b/src/items/held-items/turn-end-status.ts @@ -0,0 +1,40 @@ +import type Pokemon from "#app/field/pokemon"; +import { HeldItem, ITEM_EFFECT } from "#app/items/held-item"; +import type { StatusEffect } from "#enums/status-effect"; +import type { HeldItemId } from "#enums/held-item-id"; + +export interface TURN_END_STATUS_PARAMS { + /** The pokemon with the item */ + pokemon: Pokemon; +} + +/** + * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a + * set {@linkcode StatusEffect} at the end of a turn. + * @extends PokemonHeldItemModifier + * @see {@linkcode apply} + */ +export class TurnEndStatusHeldItem extends HeldItem { + public effects: ITEM_EFFECT[] = [ITEM_EFFECT.TURN_END_STATUS]; + /** The status effect to be applied by the held item */ + private effect: StatusEffect; + + constructor(type: HeldItemId, maxStackCount = 1, effect: StatusEffect) { + super(type, maxStackCount); + + this.effect = effect; + } + + /** + * Tries to inflicts the holder with the associated {@linkcode StatusEffect}. + * @param pokemon {@linkcode Pokemon} that holds the held item + * @returns `true` if the status effect was applied successfully + */ + apply(params: TURN_END_STATUS_PARAMS): boolean { + return params.pokemon.trySetStatus(this.effect, true, undefined, undefined, this.name); + } + + getStatusEffect(): StatusEffect { + return this.effect; + } +} diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 1d2db5c5b63..c3907ef9456 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -13,7 +13,6 @@ import { LearnMovePhase, LearnMoveType } from "#app/phases/learn-move-phase"; import { LevelUpPhase } from "#app/phases/level-up-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import type { VoucherType } from "#app/system/voucher"; -import { Command } from "#app/ui/command-ui-handler"; import { addTextObject, TextStyle } from "#app/ui/text"; import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -42,7 +41,6 @@ import { getModifierType, ModifierTypeGenerator, modifierTypes, - PokemonHeldItemModifierType, } from "./modifier-type"; import { Color, ShadowColor } from "#enums/color"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; @@ -1029,438 +1027,6 @@ export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { } } -/** - * Modifier used for held items that apply critical-hit stage boost(s). - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class CritBoosterModifier extends PokemonHeldItemModifier { - /** The amount of stages by which the held item increases the current critical-hit stage value */ - protected stageIncrement: number; - - constructor(type: ModifierType, pokemonId: number, stageIncrement: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.stageIncrement = stageIncrement; - } - - clone() { - return new CritBoosterModifier(this.type, this.pokemonId, this.stageIncrement, this.stackCount); - } - - getArgs(): any[] { - return super.getArgs().concat(this.stageIncrement); - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof CritBoosterModifier) { - return (modifier as CritBoosterModifier).stageIncrement === this.stageIncrement; - } - - return false; - } - - /** - * Increases the current critical-hit stage value by {@linkcode stageIncrement}. - * @param _pokemon {@linkcode Pokemon} N/A - * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns always `true` - */ - override apply(_pokemon: Pokemon, critStage: NumberHolder): boolean { - critStage.value += this.stageIncrement; - return true; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } -} - -/** - * Modifier used for held items that apply critical-hit stage boost(s) - * if the holder is of a specific {@linkcode SpeciesId}. - * @extends CritBoosterModifier - * @see {@linkcode shouldApply} - */ -export class SpeciesCritBoosterModifier extends CritBoosterModifier { - /** The species that the held item's critical-hit stage boost applies to */ - private species: SpeciesId[]; - - constructor( - type: ModifierType, - pokemonId: number, - stageIncrement: number, - species: SpeciesId[], - stackCount?: number, - ) { - super(type, pokemonId, stageIncrement, stackCount); - - this.species = species; - } - - clone() { - return new SpeciesCritBoosterModifier( - this.type, - this.pokemonId, - this.stageIncrement, - this.species, - this.stackCount, - ); - } - - getArgs(): any[] { - return [...super.getArgs(), this.species]; - } - - matchType(modifier: Modifier): boolean { - return modifier instanceof SpeciesCritBoosterModifier; - } - - /** - * Checks if the holder's {@linkcode SpeciesId} (or its fused species) is listed - * in {@linkcode species}. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param critStage {@linkcode NumberHolder} that holds the resulting critical-hit level - * @returns `true` if the critical-hit level can be incremented, false otherwise - */ - override shouldApply(pokemon: Pokemon, critStage: NumberHolder): boolean { - return ( - super.shouldApply(pokemon, critStage) && - (this.species.includes(pokemon.getSpeciesForm(true).speciesId) || - (pokemon.isFusion() && this.species.includes(pokemon.getFusionSpeciesForm(true).speciesId))) - ); - } -} - -/** - * Applies Specific Type item boosts (e.g., Magnet) - */ -export class AttackTypeBoosterModifier extends PokemonHeldItemModifier { - public moveType: PokemonType; - private boostMultiplier: number; - - constructor(type: ModifierType, pokemonId: number, moveType: PokemonType, boostPercent: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.moveType = moveType; - this.boostMultiplier = boostPercent * 0.01; - } - - matchType(modifier: Modifier): boolean { - if (modifier instanceof AttackTypeBoosterModifier) { - const attackTypeBoosterModifier = modifier as AttackTypeBoosterModifier; - return ( - attackTypeBoosterModifier.moveType === this.moveType && - attackTypeBoosterModifier.boostMultiplier === this.boostMultiplier - ); - } - - return false; - } - - clone() { - return new AttackTypeBoosterModifier( - this.type, - this.pokemonId, - this.moveType, - this.boostMultiplier * 100, - this.stackCount, - ); - } - - getArgs(): any[] { - return super.getArgs().concat([this.moveType, this.boostMultiplier * 100]); - } - - /** - * Checks if {@linkcode AttackTypeBoosterModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the held item - * @param moveType the {@linkcode PokemonType} of the move being used - * @param movePower the {@linkcode NumberHolder} that holds the power of the move - * @returns `true` if boosts should be applied to the move. - */ - override shouldApply(pokemon?: Pokemon, moveType?: PokemonType, movePower?: NumberHolder): boolean { - return ( - super.shouldApply(pokemon, moveType, movePower) && - typeof moveType === "number" && - movePower instanceof NumberHolder && - this.moveType === moveType - ); - } - - /** - * Applies {@linkcode AttackTypeBoosterModifier} - * @param pokemon {@linkcode Pokemon} that holds the held item - * @param moveType {@linkcode PokemonType} of the move being used - * @param movePower {@linkcode NumberHolder} that holds the power of the move - * @returns `true` if boosts have been applied to the move. - */ - override apply(_pokemon: Pokemon, moveType: PokemonType, movePower: NumberHolder): boolean { - if (moveType === this.moveType && movePower.value >= 1) { - (movePower as NumberHolder).value = Math.floor( - (movePower as NumberHolder).value * (1 + this.getStackCount() * this.boostMultiplier), - ); - return true; - } - - return false; - } - - getScoreMultiplier(): number { - return 1.2; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 99; - } -} - -export class SurviveDamageModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier): boolean { - return modifier instanceof SurviveDamageModifier; - } - - clone() { - return new SurviveDamageModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if the {@linkcode SurviveDamageModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage - * @returns `true` if the {@linkcode SurviveDamageModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, surviveDamage?: BooleanHolder): boolean { - return super.shouldApply(pokemon, surviveDamage) && !!surviveDamage; - } - - /** - * Applies {@linkcode SurviveDamageModifier} - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param surviveDamage {@linkcode BooleanHolder} that holds the survive damage - * @returns `true` if the survive damage has been applied - */ - override apply(pokemon: Pokemon, surviveDamage: BooleanHolder): boolean { - if (!surviveDamage.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - surviveDamage.value = true; - - globalScene.queueMessage( - i18next.t("modifier:surviveDamageApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - ); - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 5; - } -} - -export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof BypassSpeedChanceModifier; - } - - clone() { - return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if {@linkcode BypassSpeedChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { - return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; - } - - /** - * Applies {@linkcode BypassSpeedChanceModifier} - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied - */ - override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean { - if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - doBypassSpeed.value = true; - const isCommandFight = - globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; - const hasQuickClaw = this.type instanceof PokemonHeldItemModifierType && this.type.id === "QUICK_CLAW"; - - if (isCommandFight && hasQuickClaw) { - globalScene.queueMessage( - i18next.t("modifier:bypassSpeedChanceApply", { - pokemonName: getPokemonNameWithAffix(pokemon), - itemName: i18next.t("modifierType:ModifierType.QUICK_CLAW.name"), - }), - ); - } - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -/** - * Class for Pokemon held items like King's Rock - * Because King's Rock can be stacked in PokeRogue, unlike mainline, it does not receive a boost from AbilityId.SERENE_GRACE - */ -export class FlinchChanceModifier extends PokemonHeldItemModifier { - private chance: number; - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - this.chance = 10; - } - - matchType(modifier: Modifier) { - return modifier instanceof FlinchChanceModifier; - } - - clone() { - return new FlinchChanceModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Checks if {@linkcode FlinchChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param flinched {@linkcode BooleanHolder} that is `true` if the pokemon flinched - * @returns `true` if {@linkcode FlinchChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, flinched?: BooleanHolder): boolean { - return super.shouldApply(pokemon, flinched) && !!flinched; - } - - /** - * Applies {@linkcode FlinchChanceModifier} to randomly flinch targets hit. - * @param pokemon - The {@linkcode Pokemon} that holds the item - * @param flinched - A {@linkcode BooleanHolder} holding whether the pokemon has flinched - * @returns `true` if {@linkcode FlinchChanceModifier} was applied successfully - */ - override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean { - // The check for pokemon.summonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch - // TODO: Since summonData is always defined now, we can probably remove this - if (pokemon.summonData && !flinched.value && pokemon.randBattleSeedInt(100) < this.getStackCount() * this.chance) { - flinched.value = true; - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 3; - } -} - -export class TurnHealModifier extends PokemonHeldItemModifier { - matchType(modifier: Modifier) { - return modifier instanceof TurnHealModifier; - } - - clone() { - return new TurnHealModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Applies {@linkcode TurnHealModifier} - * @param pokemon The {@linkcode Pokemon} that holds the item - * @returns `true` if the {@linkcode Pokemon} was healed - */ - override apply(pokemon: Pokemon): boolean { - if (!pokemon.isFullHp()) { - globalScene.unshiftPhase( - new PokemonHealPhase( - pokemon.getBattlerIndex(), - toDmgValue(pokemon.getMaxHp() / 16) * this.stackCount, - i18next.t("modifier:turnHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - true, - ), - ); - return true; - } - - return false; - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 4; - } -} - -/** - * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a - * set {@linkcode StatusEffect} at the end of a turn. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} - */ -export class TurnStatusEffectModifier extends PokemonHeldItemModifier { - /** The status effect to be applied by the held item */ - private effect: StatusEffect; - - constructor(type: ModifierType, pokemonId: number, stackCount?: number) { - super(type, pokemonId, stackCount); - - switch (type.id) { - case "TOXIC_ORB": - this.effect = StatusEffect.TOXIC; - break; - case "FLAME_ORB": - this.effect = StatusEffect.BURN; - break; - } - } - - /** - * Checks if {@linkcode modifier} is an instance of this class, - * intentionally ignoring potentially different {@linkcode effect}s - * to prevent held item stockpiling since the item obtained first - * would be the only item able to {@linkcode apply} successfully. - * @override - * @param modifier {@linkcode Modifier} being type tested - * @return `true` if {@linkcode modifier} is an instance of - * TurnStatusEffectModifier, false otherwise - */ - matchType(modifier: Modifier): boolean { - return modifier instanceof TurnStatusEffectModifier; - } - - clone() { - return new TurnStatusEffectModifier(this.type, this.pokemonId, this.stackCount); - } - - /** - * Tries to inflicts the holder with the associated {@linkcode StatusEffect}. - * @param pokemon {@linkcode Pokemon} that holds the held item - * @returns `true` if the status effect was applied successfully - */ - override apply(pokemon: Pokemon): boolean { - return pokemon.trySetStatus(this.effect, true, undefined, undefined, this.type.name); - } - - getMaxHeldItemCount(_pokemon: Pokemon): number { - return 1; - } - - getStatusEffect(): StatusEffect { - return this.effect; - } -} - export class HitHealModifier extends PokemonHeldItemModifier { matchType(modifier: Modifier) { return modifier instanceof HitHealModifier; diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 637953a472c..69e96383a57 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -57,8 +57,6 @@ import { DamageMoneyRewardModifier, EnemyAttackStatusEffectChanceModifier, EnemyEndureChanceModifier, - FlinchChanceModifier, - HitHealModifier, PokemonMultiHitModifier, } from "#app/modifier/modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; @@ -456,7 +454,7 @@ export class MoveEffectPhase extends PokemonPhase { if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.hitsSubstitute(user, target)) { const flinched = new BooleanHolder(false); - globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + applyHeldItems(ITEM_EFFECT.FLINCH_CHANCE, { pokemon: user, flinched: flinched }); if (flinched.value) { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.id, user.id); } diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 0ce786966f7..93c91fef34d 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -8,7 +8,6 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { EnemyTurnHealModifier, EnemyStatusEffectHealChanceModifier, - TurnStatusEffectModifier, TurnHeldItemTransferModifier, } from "#app/modifier/modifier"; import i18next from "i18next"; @@ -55,7 +54,7 @@ export class TurnEndPhase extends FieldPhase { applyPostTurnAbAttrs(PostTurnAbAttr, pokemon); } - globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); + applyHeldItems(ITEM_EFFECT.TURN_END_STATUS, { pokemon: pokemon }); globalScene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); pokemon.tempSummonData.turnCount++; diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 2d009b30bf3..76709f85e94 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -5,7 +5,6 @@ import { AbilityId } from "#enums/ability-id"; import { Stat } from "#app/enums/stat"; import type Pokemon from "#app/field/pokemon"; import { PokemonMove } from "#app/field/pokemon"; -import { BypassSpeedChanceModifier } from "#app/modifier/modifier"; import { Command } from "#app/ui/command-ui-handler"; import { randSeedShuffle, BooleanHolder } from "#app/utils/common"; import { AttemptCapturePhase } from "./attempt-capture-phase"; @@ -23,6 +22,8 @@ import { TrickRoomTag } from "#app/data/arena-tag"; import { SwitchType } from "#enums/switch-type"; import { globalScene } from "#app/global-scene"; import { TeraPhase } from "./tera-phase"; +import { ITEM_EFFECT } from "#app/items/held-item"; +import { applyHeldItems } from "#app/items/all-held-items"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; @@ -80,7 +81,7 @@ export class TurnStartPhase extends FieldPhase { applyAbAttrs(BypassSpeedChanceAbAttr, p, null, false, bypassSpeed); applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, false, bypassSpeed, canCheckHeldItems); if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); + applyHeldItems(ITEM_EFFECT.BYPASS_SPEED_CHANCE, { pokemon: p, doBypassSpeed: bypassSpeed }); } battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; });