diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 5797fda5611..a9fa022c1eb 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -170,6 +170,7 @@ import { StatusEffect } from "#enums/status-effect"; import { initGlobalScene } from "#app/global-scene"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { HideAbilityPhase } from "#app/phases/hide-ability-phase"; +import { applyChallenges, ChallengeType } from "./data/challenge"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -1082,6 +1083,8 @@ export default class BattleScene extends SceneBase { postProcess(pokemon); } + applyChallenges(ChallengeType.ENEMY_POKEMON_MODIFY, pokemon); + for (let i = 0; i < pokemon.ivs.length; i++) { if (OPP_IVS_OVERRIDE_VALIDATED[i] > -1) { pokemon.ivs[i] = OPP_IVS_OVERRIDE_VALIDATED[i]; diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index e49bd049cd6..bc35d39e7cd 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -14,6 +14,8 @@ import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifi import { SpeciesFormKey } from "#enums/species-form-key"; import { speciesStarterCosts } from "./starters"; import i18next from "i18next"; +import { Challenges } from "#enums/challenges"; +import { Stat } from "#enums/stat"; export enum SpeciesWildEvolutionDelay { @@ -90,7 +92,7 @@ export class SpeciesFormEvolution { public evoFormKey: string | null; public level: number; public item: EvolutionItem | null; - public condition: SpeciesEvolutionCondition | null; + public condition: SpeciesEvolutionCondition | null; // TODO: Add a ChallengeType to change evolution conditions based on what kind of condition it is (use an enum) public wildDelay: SpeciesWildEvolutionDelay; public description = ""; @@ -179,7 +181,8 @@ class TimeOfDayEvolutionCondition extends SpeciesEvolutionCondition { class MoveEvolutionCondition extends SpeciesEvolutionCondition { public move: Moves; constructor(move: Moves) { - super(p => p.moveset.filter(m => m.moveId === move).length > 0); + // TODO: Remove deprecated Challenge check + super(p => p.moveset.filter(m => m.moveId === move).length > 0 || globalScene.gameMode.hasChallenge(Challenges.METRONOME)); this.move = move; const moveKey = Moves[this.move].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join(""); this.description = i18next.t("pokemonEvolutions:move", { move: i18next.t(`move:${moveKey}.name`) }); @@ -218,7 +221,13 @@ class FriendshipMoveTypeEvolutionCondition extends SpeciesEvolutionCondition { public amount: number; public type: PokemonType; constructor(amount: number, type: PokemonType) { - super(p => p.friendship >= amount && !!p.getMoveset().find(m => m?.getMove().type === type)); + // TODO: Remove deprecated Challenge check + super(p => + p.friendship >= amount && + (!!p.getMoveset().find(m => m?.getMove().type === type || + (globalScene.gameMode.hasChallenge(Challenges.METRONOME) && + !!globalScene.getPlayerParty().find(p => p.getTypes(false, false, true).indexOf(type) > -1) + )))); this.amount = amount; this.type = type; this.description = i18next.t("pokemonEvolutions:friendshipMoveType", { type: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`) }); @@ -262,7 +271,10 @@ class WeatherEvolutionCondition extends SpeciesEvolutionCondition { class MoveTypeEvolutionCondition extends SpeciesEvolutionCondition { public type: PokemonType; constructor(type: PokemonType) { - super(p => p.moveset.filter(m => m?.getMove().type === type).length > 0); + // TODO: Remove deprecated Challenge check + super(p => p.moveset.filter(m => m?.getMove().type === type).length > 0 || + (globalScene.gameMode.hasChallenge(Challenges.METRONOME) && + !!globalScene.getPlayerParty().find(p => p.getTypes(false, false, true).indexOf(type) > -1))); this.type = type; this.description = i18next.t("pokemonEvolutions:moveType", { type: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`) }); } @@ -281,7 +293,13 @@ class TreasureEvolutionCondition extends SpeciesEvolutionCondition { class TyrogueEvolutionCondition extends SpeciesEvolutionCondition { public move: Moves; constructor(move: Moves) { + // TODO: Remove deprecated Challenge check super(p => + (globalScene.gameMode.hasChallenge(Challenges.METRONOME) && ( // Metronome mode = no moves, do it the old fashioned way + (move === Moves.LOW_SWEEP && p.stats[Stat.ATK] > p.stats[Stat.DEF]) || + (move === Moves.MACH_PUNCH && p.stats[Stat.DEF] > p.stats[Stat.ATK]) || + (move === Moves.RAPID_SPIN && p.stats[Stat.DEF] === p.stats[Stat.ATK]) + )) || p.getMoveset(true).find(m => m && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(m.moveId))?.moveId === move); this.move = move; const moveKey = Moves[this.move].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join(""); @@ -302,12 +320,19 @@ class MoveTimeOfDayEvolutionCondition extends SpeciesEvolutionCondition { public move: Moves; public timesOfDay: TimeOfDay[]; constructor(move: Moves, tod: "day" | "night") { + // TODO: Remove deprecated Challenge check if (tod === "day") { - super(p => p.moveset.filter(m => m.moveId === move).length > 0 && (globalScene.arena.getTimeOfDay() === TimeOfDay.DAWN || globalScene.arena.getTimeOfDay() === TimeOfDay.DAY)); + super(p => + (p.moveset.filter(m => m.moveId === move).length > 0 || + globalScene.gameMode.hasChallenge(Challenges.METRONOME)) && + (globalScene.arena.getTimeOfDay() === TimeOfDay.DAWN || globalScene.arena.getTimeOfDay() === TimeOfDay.DAY)); this.move = move; this.timesOfDay = [ TimeOfDay.DAWN, TimeOfDay.DAY ]; } else if (tod === "night") { - super(p => p.moveset.filter(m => m.moveId === move).length > 0 && (globalScene.arena.getTimeOfDay() === TimeOfDay.DUSK || globalScene.arena.getTimeOfDay() === TimeOfDay.NIGHT)); + super(p => + (p.moveset.filter(m => m.moveId === move).length > 0 || + globalScene.gameMode.hasChallenge(Challenges.METRONOME)) && + (globalScene.arena.getTimeOfDay() === TimeOfDay.DUSK || globalScene.arena.getTimeOfDay() === TimeOfDay.NIGHT)); this.move = move; this.timesOfDay = [ TimeOfDay.DUSK, TimeOfDay.NIGHT ]; } else { @@ -330,9 +355,10 @@ class BiomeEvolutionCondition extends SpeciesEvolutionCondition { class DunsparceEvolutionCondition extends SpeciesEvolutionCondition { constructor() { + // TODO: Remove deprecated Challenge check super(p => { let ret = false; - if (p.moveset.filter(m => m.moveId === Moves.HYPER_DRILL).length > 0) { + if (p.moveset.filter(m => m.moveId === Moves.HYPER_DRILL).length > 0 || globalScene.gameMode.hasChallenge(Challenges.METRONOME)) { globalScene.executeWithSeedOffset(() => ret = !Utils.randSeedInt(4), p.id); } return ret; diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 455421ffefd..6901406215f 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -6,7 +6,7 @@ import type PokemonSpecies from "#app/data/pokemon-species"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { speciesStarterCosts } from "#app/data/balance/starters"; import type Pokemon from "#app/field/pokemon"; -import { PokemonMove } from "#app/field/pokemon"; +import { type EnemyPokemon, PokemonMove } from "#app/field/pokemon"; import type { FixedBattleConfig } from "#app/battle"; import { ClassicFixedBossWaves, BattleType, getRandomTrainerFunc } from "#app/battle"; import Trainer, { TrainerVariant } from "#app/field/trainer"; @@ -15,12 +15,14 @@ import { Challenges } from "#enums/challenges"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import { Nature } from "#enums/nature"; -import type { Moves } from "#enums/moves"; +import { Moves } from "#enums/moves"; import { TypeColor, TypeShadow } from "#enums/color"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { globalScene } from "#app/global-scene"; import { pokemonFormChanges } from "./pokemon-forms"; import { pokemonEvolutions } from "./balance/pokemon-evolutions"; +import { ModifierPoolType, type ModifierPool, type ModifierTypeOption } from "#app/modifier/modifier-type"; +import type { LearnMoveType } from "#app/phases/learn-move-phase"; /** A constant for the default max cost of the starting party before a run */ const DEFAULT_PARTY_MAX_COST = 10; @@ -93,6 +95,26 @@ export enum ChallengeType { * Modifies what the pokemon stats for Flip Stat Mode. */ FLIP_STAT, + /** + * Modifies enemy mons AFTER post process function + */ + ENEMY_POKEMON_MODIFY, + /** + * Prevents the learning of moves + */ + BAN_MOVE_LEARNING, + /** + * Negates PP Usage + */ + MODIFY_PP_USE, + /** + * Modifies modifier pools of specified type + */ + MODIFIER_POOL_MODIFY, + /** + * Modifies the shop options + */ + SHOP_MODIFY, } /** @@ -426,6 +448,57 @@ export abstract class Challenge { applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) { return false; } + + /** + * An apply function for ENEMY_POKEMON_MODIFY. Derived classes should alter this. + * @param _pokemon {@link EnemyPokemon} the mon to be modified + * @returns {@link boolean} Whether this function did anything. + */ + applyEnemyPokemonModify(_pokemon: EnemyPokemon) { + return false; + } + + /** + * An apply function for BAN_MOVE_LEARNING. Derived classes should alter this. + * @param _pokemon {@link Pokemon} Pokemon who wants to learn the move + * @param _move {@link Moves} Move being learned + * @param _learnType {@link LearnMoveType} How the move is being learned + * @param _valid: {@link BooleanHolder} Whether the move is valid for this challenge + * @returns {@link boolean} Whether the move should be restricted from learning + */ + applyBanMoveLearning(_pokemon: Pokemon, _move: Moves, _learnType: LearnMoveType, _valid: Utils.BooleanHolder) { + return false; + } + + /** + * An apply function for MODIFY_PP_USE. Derived classes should alter this. + * @param _pokemon {@link Pokemon} Pokemon using the move + * @param _move {@link Moves} Move being used + * @param _usedPP {@link Utils.NumberHolder} Holds the value associated with how much PP should be used + * @returns {@link boolean} Whether this function did anything. + */ + applyModifyPPUsage(_pokemon: Pokemon, _move: Moves, _usedPP: Utils.NumberHolder) { + return false; + } + + /** + * An apply function for MODIFIER_POOL_MODIFY. Derived classes should alter this. + * @param _poolType {@link ModifierPoolType} What kind of item pool + * @param _modifierPool {@link ModifierPool} Pool to modify + * @returns {@link boolean} Whether this function did anything. + */ + applyModifierPoolModify(_poolType: ModifierPoolType, _modifierPool: ModifierPool) { + return false; + } + + /** + * An apply function for SHOP_MODIFY. Derived classes should alter this. + * @param _options {@link ModifierTypeOption} Array of shop options + * @returns {@link boolean} Whether this function did anything. + */ + applyShopModify(_options: ModifierTypeOption[]) { + return false; + } } type ChallengeCondition = (data: GameData) => boolean; @@ -833,6 +906,29 @@ export class FreshStartChallenge extends Challenge { return true; } + override applyModifierPoolModify(poolType: ModifierPoolType, modifierPool: ModifierPool): boolean { + if (poolType !== ModifierPoolType.PLAYER) { + return false; + } + let ret = false; + + let idx; + const bans = ["EVIOLITE", "MINI_BLACK_HOLE"]; + const t = [ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.ULTRA, ModifierTier.ROGUE, ModifierTier.MASTER]; + for (let i = 0; i < t.length; i++) { + idx = 0; + while (idx > -1) { + idx = modifierPool[t[i]].findIndex(p => bans.includes(p.modifierType.id)); + if (idx > -1) { + modifierPool[t[i]].splice(idx, 1); + ret = true; + } + } + } + + return ret; + } + override getDifficulty(): number { return 0; } @@ -864,6 +960,14 @@ export class InverseBattleChallenge extends Challenge { return 0; } + override applyEnemyPokemonModify(pokemon: EnemyPokemon): boolean { + if (pokemon.species.speciesId === Species.ETERNATUS) { + pokemon.moveset[2] = new PokemonMove(Moves.THUNDERBOLT); + return true; + } + return false; + } + applyTypeEffectiveness(effectiveness: Utils.NumberHolder): boolean { if (effectiveness.value < 1) { effectiveness.value = 2; @@ -905,6 +1009,97 @@ export class FlipStatChallenge extends Challenge { } } +export class MetronomeChallenge extends Challenge { + constructor() { + super(Challenges.METRONOME, 1); + } + + override applyStarterModify(pokemon: Pokemon): boolean { + pokemon.moveset = [new PokemonMove(Moves.METRONOME, 0, 3)]; + return true; + } + + override applyEnemyPokemonModify(pokemon: EnemyPokemon): boolean { + pokemon.moveset = [new PokemonMove(Moves.METRONOME, 0, 3)]; + return true; + } + + override applyBanMoveLearning( + _pokemon: Pokemon, + _move: Moves, + _learnType: LearnMoveType, + valid: Utils.BooleanHolder, + ): boolean { + valid.value = false; + return true; + } + + /** + * Makes sure 0 PP is used, called when applying other PP usage modifiers such as Pressure + * @param _pokemon {@link Pokemon} unused + * @param _move {@link Moves} unused + * @param usedPP + * @returns true + */ + override applyModifyPPUsage(_pokemon: Pokemon, _move: Moves, usedPP: Utils.NumberHolder): boolean { + usedPP.value = 0; + return true; + } + + override applyModifierPoolModify(poolType: ModifierPoolType, modifierPool: ModifierPool): boolean { + if (poolType !== ModifierPoolType.PLAYER) { + return false; + } + let ret = false; + + let idx; + const bans = [ + "TM_COMMON", + "ETHER", + "MAX_ETHER", + "ELIXIR", + "MAX_ELIXIR", + "PP_UP", + "MEMORY_MUSHROOM", + "TM_GREAT", + "TM_ULTRA", + "PP_MAX", + ]; + const t = [ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.ULTRA, ModifierTier.ROGUE, ModifierTier.MASTER]; + for (let i = 0; i < t.length; i++) { + idx = 0; + while (idx > -1) { + idx = modifierPool[t[i]].findIndex(p => bans.includes(p.modifierType.id)); + if (idx > -1) { + modifierPool[t[i]].splice(idx, 1); + ret = true; + } + } + } + + return ret; + } + + override applyShopModify(options: ModifierTypeOption[]): boolean { + const removals = ["ETHER", "MAX_ETHER", "ELIXIR", "MAX_ELIXIR", "MEMORY_MUSHROOM"]; // Pending rework, these need to match locale key + const opstart = options.length; + removals.map(r => { + const idx = options.findIndex(o => o.type.localeKey.split(".")[1] === r); // Currently the quickest way to get the id + if (idx >= 0) { + options.splice(idx, 1); + } + }); + return opstart > options.length; + } + + static loadChallenge(source: MetronomeChallenge | any): MetronomeChallenge { + const newChallenge = new MetronomeChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + /** * Lowers the amount of starter points available. */ @@ -1125,6 +1320,66 @@ export function applyChallenges( export function applyChallenges(challengeType: ChallengeType.FLIP_STAT, pokemon: Pokemon, baseStats: number[]): boolean; +/** + * Apply all challenges that modify Enemy Pokemon generation (after post process funcs) + * @param challengeType {@link ChallengeType} ChallengeType.ENEMY_POKEMON_MODIFY + * @param pokemon {@link EnemyPokemon} Pokemon to be modified + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.ENEMY_POKEMON_MODIFY, pokemon: EnemyPokemon): boolean; + +/** + * Apply all challenges that restrict Pokemon from learning certain moves + * @param challengeType {@link ChallengeType} ChallengeType.BAN_MOVE_LEARNING + * @param pokemon {@link Pokemon} The mon attempting to learn + * @param move {@link Moves} The move being learned + * @param learnType {@link LearnMoveType} how the move is being learned + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.BAN_MOVE_LEARNING, + pokemon: Pokemon, + move: Moves, + learnType: LearnMoveType, + valid: Utils.BooleanHolder, +): boolean; + +/** + * Apply all challenges that modify how much PP is used + * @param challengeType {@link ChallengeType} ChallengeType.MODIFY_PP_USE + * @param pokemon {@link Pokemon} Pokemon using the move + * @param move {@link Moves} Move being used + * @param usedPP {@link Utils.NumberHolder} Holds the value associated with how much PP should be used + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.MODIFY_PP_USE, + pokemon: Pokemon, + move: Moves, + usedPP: Utils.NumberHolder, +): boolean; + +/** + * Apply all challenges that modify modifier pools //TODO: Modifier rework will need to look at this + * @param challengeType {@link ChallengeType} ChallengeType.MODIFIER_POOL_MODIFY + * @param poolType {@link ModifierPoolType} Which kind of pool is being changed (wild held items, player rewards etc) + * @param modifierPool {@link ModifierPool} The item pool the challenge may attempt to modify + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.MODIFIER_POOL_MODIFY, + poolType: ModifierPoolType, + modifierPool: ModifierPool, +): boolean; + +/** + * Apply all challenges that modify the shop + * @param challengeType ChallengeType.SHOP_MODIFY + * @param options {@link ModifierTypeOption} List of shop options including the prices + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.SHOP_MODIFY, options: ModifierTypeOption[]): boolean; + export function applyChallenges(challengeType: ChallengeType, ...args: any[]): boolean { let ret = false; globalScene.gameMode.challenges.forEach(c => { @@ -1172,6 +1427,21 @@ export function applyChallenges(challengeType: ChallengeType, ...args: any[]): b case ChallengeType.FLIP_STAT: ret ||= c.applyFlipStat(args[0], args[1]); break; + case ChallengeType.ENEMY_POKEMON_MODIFY: + ret ||= c.applyEnemyPokemonModify(args[0]); + break; + case ChallengeType.BAN_MOVE_LEARNING: + ret ||= c.applyBanMoveLearning(args[0], args[1], args[2], args[3]); + break; + case ChallengeType.MODIFY_PP_USE: + ret ||= c.applyModifyPPUsage(args[0], args[1], args[2]); + break; + case ChallengeType.MODIFIER_POOL_MODIFY: + ret ||= c.applyModifierPoolModify(args[0], args[1]); + break; + case ChallengeType.SHOP_MODIFY: + ret ||= c.applyShopModify(args[0]); + break; } } }); @@ -1199,6 +1469,8 @@ export function copyChallenge(source: Challenge | any): Challenge { return InverseBattleChallenge.loadChallenge(source); case Challenges.FLIP_STAT: return FlipStatChallenge.loadChallenge(source); + case Challenges.METRONOME: + return MetronomeChallenge.loadChallenge(source); } throw new Error("Unknown challenge copied"); } @@ -1212,6 +1484,7 @@ export function initChallenges() { new FreshStartChallenge(), new InverseBattleChallenge(), new FlipStatChallenge(), + new MetronomeChallenge(), ); } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index a05d63b3d3d..5727612e059 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8090,7 +8090,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { return false; } const userTypes = user.getTypes(); - const validTypes = this.getTypeResistances(globalScene.gameMode, moveData.type).filter(t => !userTypes.includes(t)); // valid types are ones that are not already the user's types + const validTypes = this.getTypeResistances(moveData.type).filter(t => !userTypes.includes(t)); // valid types are ones that are not already the user's types if (!validTypes.length) { return false; } @@ -8106,7 +8106,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { * Retrieve the types resisting a given type. Used by Conversion 2 * @returns An array populated with Types, or an empty array if no resistances exist (Unknown or Stellar type) */ - getTypeResistances(gameMode: GameMode, type: number): PokemonType[] { + getTypeResistances(type: number): PokemonType[] { const typeResistances: PokemonType[] = []; for (let i = 0; i < Object.keys(PokemonType).length; i++) { 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 1e4c9a3b957..327fb6ab7b9 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -54,6 +54,7 @@ import { allMoves } from "#app/data/moves/move"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/bugTypeSuperfan"; @@ -190,6 +191,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde ) .withMaxAllowedEncounters(1) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withDisallowedChallenges(Challenges.METRONOME) .withIntroSpriteConfigs([]) // These are set in onInit() .withAutoHideIntroVisuals(false) .withIntroDialogue([ diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index eca99fc0c13..aabb2e6de8f 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -80,7 +80,7 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder MysteryEncounterType.CLOWNING_AROUND, ) .withEncounterTier(MysteryEncounterTier.ULTRA) - .withDisallowedChallenges(Challenges.SINGLE_TYPE) + .withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.METRONOME) .withSceneWaveRangeRequirement(80, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withAnimations(EncounterAnim.SMOKESCREEN) .withAutoHideIntroVisuals(false) diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 75527e1f8c1..1bce16400b7 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -41,6 +41,7 @@ import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; import i18next from "i18next"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/dancingLessons"; @@ -99,6 +100,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder ) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withDisallowedChallenges(Challenges.METRONOME) .withIntroSpriteConfigs([]) // Uses a real Pokemon sprite instead of ME Intro Visuals .withAnimations(EncounterAnim.DANCE) .withHideWildIntroMessage(true) diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts index 9b8e2e24d12..1e8f95adaf2 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -11,6 +11,11 @@ import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounte import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { CanLearnTMRequirement } from "../mystery-encounter-requirements"; +import { globalScene } from "#app/global-scene"; +import { Challenges } from "#enums/challenges"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/departmentStoreSale"; @@ -55,34 +60,37 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu .withTitle(`${namespace}:title`) .withDescription(`${namespace}:description`) .withQuery(`${namespace}:query`) - .withSimpleOption( - { - buttonLabel: `${namespace}:option.1.label`, - buttonTooltip: `${namespace}:option.1.tooltip`, - }, - async () => { - // Choose TMs - const modifiers: ModifierTypeFunc[] = []; - let i = 0; - while (i < 5) { - // 2/2/1 weight on TM rarity - const roll = randSeedInt(5); - if (roll < 2) { - modifiers.push(modifierTypes.TM_COMMON); - } else if (roll < 4) { - modifiers.push(modifierTypes.TM_GREAT); - } else { - modifiers.push(modifierTypes.TM_ULTRA); + .withOption( + MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new CanLearnTMRequirement()) + .withDialogue({ + buttonLabel: `${namespace}:option.1.label`, + buttonTooltip: `${namespace}:option.1.tooltip`, + }) + .withOptionPhase(async () => { + // Choose TMs + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 5) { + // 2/2/1 weight on TM rarity + const roll = randSeedInt(5); + if (roll < 2) { + modifiers.push(modifierTypes.TM_COMMON); + } else if (roll < 4) { + modifiers.push(modifierTypes.TM_GREAT); + } else { + modifiers.push(modifierTypes.TM_ULTRA); + } + i++; } - i++; - } - setEncounterRewards({ - guaranteedModifierTypeFuncs: modifiers, - fillRemaining: false, - }); - leaveEncounterWithoutBattle(); - }, + setEncounterRewards({ + guaranteedModifierTypeFuncs: modifiers, + fillRemaining: false, + }); + leaveEncounterWithoutBattle(); + }) + .build(), ) .withSimpleOption( { @@ -96,7 +104,7 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu while (i < 3) { // 2/1 weight on base stat booster vs PP Up const roll = randSeedInt(3); - if (roll === 0) { + if (roll === 0 && !globalScene.gameMode.hasChallenge(Challenges.METRONOME)) { modifiers.push(modifierTypes.PP_UP); } else { modifiers.push(modifierTypes.BASE_STAT_BOOSTER); diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index c13501c4511..df2322a38b7 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -46,6 +46,7 @@ import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-en import type { PokeballType } from "#enums/pokeball"; import { doShinySparkleAnim } from "#app/field/anims"; import { TrainerType } from "#enums/trainer-type"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/globalTradeSystem"; @@ -208,6 +209,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil const encounter = globalScene.currentBattle.mysteryEncounter!; const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; + applyChallenges(ChallengeType.ENEMY_POKEMON_MODIFY, receivedPokemonData); const modifiers = tradedPokemon .getHeldItems() .filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); @@ -329,6 +331,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil const encounter = globalScene.currentBattle.mysteryEncounter!; const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; + applyChallenges(ChallengeType.ENEMY_POKEMON_MODIFY, receivedPokemonData); const modifiers = tradedPokemon .getHeldItems() .filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); diff --git a/src/data/mystery-encounters/encounters/part-timer-encounter.ts b/src/data/mystery-encounters/encounters/part-timer-encounter.ts index 61b48353997..9318a85d688 100644 --- a/src/data/mystery-encounters/encounters/part-timer-encounter.ts +++ b/src/data/mystery-encounters/encounters/part-timer-encounter.ts @@ -22,6 +22,7 @@ import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/partTimer"; @@ -36,6 +37,7 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with ) .withEncounterTier(MysteryEncounterTier.COMMON) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withDisallowedChallenges(Challenges.METRONOME) .withIntroSpriteConfigs([ { spriteKey: "part_timer_crate", diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index c189e341089..282b7bef4c2 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -30,6 +30,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode import { modifierTypes } from "#app/modifier/modifier-type"; import { PokemonType } from "#enums/pokemon-type"; import { getPokeballTintColor } from "#app/data/pokeball"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theExpertPokemonBreeder"; @@ -123,6 +124,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, ) .withEncounterTier(MysteryEncounterTier.ULTRA) + .withDisallowedChallenges(Challenges.METRONOME) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withScenePartySizeRequirement(4, 6, true) // Must have at least 4 legal pokemon in party .withIntroSpriteConfigs([]) // These are set in onInit() diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index c994c6e993f..d98db698f89 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -29,6 +29,7 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { Stat } from "#enums/stat"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theStrongStuff"; @@ -46,6 +47,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder MysteryEncounterType.THE_STRONG_STUFF, ) .withEncounterTier(MysteryEncounterTier.COMMON) + .withDisallowedChallenges(Challenges.METRONOME) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withScenePartySizeRequirement(3, 6) // Must have at least 3 pokemon in party .withMaxAllowedEncounters(1) diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index aca04ad50ed..c62a0a578c4 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -34,6 +34,7 @@ import i18next from "i18next"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theWinstrateChallenge"; @@ -47,6 +48,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter = MysteryEncounter MysteryEncounterType.THE_WINSTRATE_CHALLENGE, ) .withEncounterTier(MysteryEncounterTier.ROGUE) + .withDisallowedChallenges(Challenges.METRONOME) .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withIntroSpriteConfigs([ { diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index e60fe0ddc18..e266c2b9ffa 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -27,6 +27,7 @@ import { Moves } from "#enums/moves"; import { BattlerIndex } from "#app/battle"; import { PokemonMove } from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Challenges } from "#enums/challenges"; import { randSeedInt } from "#app/utils"; /** the i18n namespace for this encounter */ @@ -46,6 +47,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde MysteryEncounterType.TRASH_TO_TREASURE, ) .withEncounterTier(MysteryEncounterTier.ULTRA) + .withDisallowedChallenges(Challenges.METRONOME) .withSceneWaveRangeRequirement(60, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withMaxAllowedEncounters(1) .withFleeAllowed(false) diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 22ec52e976c..11e40ab0020 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -129,7 +129,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit MysteryEncounterType.WEIRD_DREAM, ) .withEncounterTier(MysteryEncounterTier.ROGUE) - .withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION) + .withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION, Challenges.METRONOME) // TODO: should reset minimum wave to 10 when there are more Rogue tiers in pool. Matching Dark Deal minimum for now. .withSceneWaveRangeRequirement(30, 140) .withIntroSpriteConfigs([ diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index f9aedf2c1a7..306521a2370 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -650,6 +650,39 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement { } } +export class CanLearnTMRequirement extends EncounterPokemonRequirement { + min: number; + excludeDisallowedPokemon: boolean; + /** + * Constructs a new CanLearnTMRequirement + * @param min Minimum number of party members who can learn a TM, defaults to 1. + * @param excludeDisallowed Whether to exclude ineligible party members, defaults to false + */ + constructor(min = 1, excludeDisallowed = false) { + super(); + this.min = min; + this.excludeDisallowedPokemon = excludeDisallowed; + } + + override meetsRequirement(): boolean { + const partyPokemon = globalScene.getPlayerParty(); + if (isNullOrUndefined(partyPokemon)) { + return false; + } + return this.queryParty(partyPokemon).length >= this.min; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + return partyPokemon.filter( + pokemon => (!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle()) && pokemon.compatibleTms.length >= 0, + ); + } + + override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { + return ["canLearnTM", ""]; + } +} + export class AbilityRequirement extends EncounterPokemonRequirement { requiredAbilities: Abilities[]; minNumberOfPokemon: number; diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index a7ffe3e26ca..b272e00d760 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -51,17 +51,23 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement { return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; } + /** + * Queries party for mons meeting move requirements + * @param partyPokemon {@link PlayerPokemon} party to query + * @returns list of {@link PlayerPokemon} that match query + */ override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { if (!this.invertQuery) { - return partyPokemon.filter(pokemon => - // every required move should be included - this.requiredMoves.every(requiredMove => this.getAllPokemonMoves(pokemon).includes(requiredMove)), + return partyPokemon.filter( + pokemon => + // every required move should be included + this.requiredMoves.length === this.getAllPokemonMoves(pokemon).length, ); } return partyPokemon.filter( pokemon => // none of the "required" moves should be included - !this.requiredMoves.some(requiredMove => this.getAllPokemonMoves(pokemon).includes(requiredMove)), + this.getAllPokemonMoves(pokemon).length === 0, ); } @@ -73,19 +79,24 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement { return pkm.getLevelMoves().map(([_level, move]) => move); } + /** + * Gets all the moves with matching criteria + * @param pkm {@link PlayerPokemon} to check + * @returns list of {@link Moves} moves matching criteria + */ private getAllPokemonMoves(pkm: PlayerPokemon): Moves[] { const allPokemonMoves: Moves[] = []; if (!this.excludeLevelMoves) { - allPokemonMoves.push(...(this.getPokemonLevelMoves(pkm) ?? [])); + allPokemonMoves.push(...(this.getPokemonLevelMoves(pkm).filter(m => this.requiredMoves.includes(m)) ?? [])); } if (!this.excludeTmMoves) { - allPokemonMoves.push(...(pkm.compatibleTms ?? [])); + allPokemonMoves.push(...pkm.generateCompatibleTms(false, false).filter(m => this.requiredMoves.includes(m))); } if (!this.excludeEggMoves) { - allPokemonMoves.push(...(pkm.getEggMoves() ?? [])); + allPokemonMoves.push(...(pkm.getEggMoves()?.filter(m => this.requiredMoves.includes(m)) ?? [])); } return allPokemonMoves; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 5c6acf43e26..9d8d055a478 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -65,6 +65,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species"; import { PokemonType } from "#enums/pokemon-type"; import { getNatureName } from "#app/data/nature"; import { getPokemonNameWithAffix } from "#app/messages"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; /** * Animates exclamation sprite over trainer's head at start of encounter @@ -394,6 +395,8 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): enemyPokemon.mysteryEncounterBattleEffects = config.mysteryEncounterBattleEffects; } + applyChallenges(ChallengeType.ENEMY_POKEMON_MODIFY, enemyPokemon); + // Requires re-priming summon data to update everything properly enemyPokemon.primeSummonData(enemyPokemon.summonData); diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index a4787e819b8..ed48a64d375 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -38,6 +38,7 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import type { Abilities } from "#enums/abilities"; import type { PokeballType } from "#enums/pokeball"; import { StatusEffect } from "#enums/status-effect"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; @@ -643,6 +644,7 @@ export async function catchPokemon( showCatchObtainMessage = true, isObtain = false, ): Promise { + applyChallenges(ChallengeType.ENEMY_POKEMON_MODIFY, pokemon); const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); if ( diff --git a/src/enums/challenges.ts b/src/enums/challenges.ts index 7b506a61a2f..2ab52a53e2a 100644 --- a/src/enums/challenges.ts +++ b/src/enums/challenges.ts @@ -6,4 +6,5 @@ export enum Challenges { FRESH_START, INVERSE_BATTLE, FLIP_STAT, + METRONOME, } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index b595d516f53..dc039746917 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -241,7 +241,7 @@ import { Species } from "#enums/species"; import { getPokemonNameWithAffix } from "#app/messages"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; import { FaintPhase } from "#app/phases/faint-phase"; -import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { LearnMovePhase, LearnMoveType } from "#app/phases/learn-move-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase"; @@ -6360,13 +6360,20 @@ export class PlayerPokemon extends Pokemon { return this.getFieldIndex(); } - generateCompatibleTms(): void { - this.compatibleTms = []; + /** + * Finds the list of TMs this PlayerPokemon is compatible with based on species, form, and challenges + * @param alsoSet Whether this PlayerPokemon's compatibleTms list should be set by this function + * @param applyChal Whether to apply challenges which would ban the mon from learning specific moves + * @returns {@link Moves} the list of compatible TM moves for this PlayerPokemon + */ + generateCompatibleTms(alsoSet: boolean = true, applyChal: boolean = true): Moves[] { + const ret: Moves[] = []; + const compatible = new Utils.BooleanHolder(false); const tms = Object.keys(tmSpecies); for (const tm of tms) { const moveId = Number.parseInt(tm) as Moves; - let compatible = false; + compatible.value = false; for (const p of tmSpecies[tm]) { if (Array.isArray(p)) { const [pkm, form] = p; @@ -6375,24 +6382,32 @@ export class PlayerPokemon extends Pokemon { (this.fusionSpecies && pkm === this.fusionSpecies.speciesId)) && form === this.getFormKey() ) { - compatible = true; + compatible.value = true; break; } } else if ( p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId) ) { - compatible = true; + compatible.value = true; break; } } if (reverseCompatibleTms.indexOf(moveId) > -1) { - compatible = !compatible; + compatible.value = !compatible.value; } - if (compatible) { - this.compatibleTms.push(moveId); + if (compatible.value && applyChal) { + applyChallenges(ChallengeType.BAN_MOVE_LEARNING, this, moveId, LearnMoveType.TM, compatible); + } + if (compatible.value) { + ret.push(moveId); } } + if (alsoSet) { + this.compatibleTms = ret; + } + + return ret; } tryPopulateMoveset(moveset: StarterMoveset): boolean { @@ -7054,14 +7069,12 @@ export class EnemyPokemon extends Pokemon { new PokemonMove(Moves.FLAMETHROWER), new PokemonMove(Moves.COSMIC_POWER), ]; - if (globalScene.gameMode.hasChallenge(Challenges.INVERSE_BATTLE)) { - this.moveset[2] = new PokemonMove(Moves.THUNDERBOLT); - } break; default: super.generateAndPopulateMoveset(); break; } + applyChallenges(ChallengeType.ENEMY_POKEMON_MODIFY, this); } /** diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index f9770e9c6cc..e724e5ce958 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -127,6 +127,7 @@ import type { PermanentStat, TempBattleStat } from "#enums/stat"; import { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; const outputModifierData = false; const useMaxWeightForOutput = false; @@ -2032,10 +2033,12 @@ export const modifierTypes = { }), BERRY: () => - new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { + new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in BerryType) { return new BerryModifierType(pregenArgs[0] as BerryType); } + const ppReduction = new NumberHolder(1); // How much PP is reduced when using a move in this game mode + applyChallenges(ChallengeType.MODIFY_PP_USE, party[0], Moves.NONE, ppReduction); const berryTypes = getEnumValues(BerryType); let randBerryType: BerryType; const rand = randSeedInt(12); @@ -2043,7 +2046,8 @@ export const modifierTypes = { randBerryType = BerryType.SITRUS; } else if (rand < 4) { randBerryType = BerryType.LUM; - } else if (rand < 6) { + } else if (rand < 6 && ppReduction.value !== 0) { + // If PP isn't reduced, it doesn't need to be restored randBerryType = BerryType.LEPPA; } else { randBerryType = berryTypes[randSeedInt(berryTypes.length - 3) + 2]; @@ -2392,7 +2396,7 @@ export const modifierTypes = { ), }; -interface ModifierPool { +export interface ModifierPool { [tier: string]: WeightedModifierType[]; } @@ -2691,7 +2695,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.AMULET_COIN, skipInLastClassicWaveOrDefault(3)), new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => { const { gameMode, gameData } = globalScene; - if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) { + if (gameMode.isDaily || gameData.isUnlocked(Unlockables.EVIOLITE)) { return party.some(p => { // Check if Pokemon's species (or fusion species, if applicable) can evolve or if they're G-Max'd if ( @@ -2948,11 +2952,7 @@ const modifierPool: ModifierPool = { ), new WeightedModifierType( modifierTypes.MINI_BLACK_HOLE, - () => - globalScene.gameMode.isDaily || - (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE)) - ? 1 - : 0, + () => (globalScene.gameMode.isDaily || globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE) ? 1 : 0), 1, ), ].map(m => { @@ -3154,6 +3154,7 @@ export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool pool = dailyStarterModifierPool; break; } + applyChallenges(ChallengeType.MODIFIER_POOL_MODIFY, poolType, pool); return pool; } @@ -3165,9 +3166,6 @@ export const itemPoolChecks: Map = new Ma export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: ModifierPoolType, rerollCount = 0) { const pool = getModifierPoolForType(poolType); - itemPoolChecks.forEach((_v, k) => { - itemPoolChecks.set(k, false); - }); const ignoredIndexes = {}; const modifierTableData = {}; @@ -3452,7 +3450,9 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseC [new ModifierTypeOption(modifierTypes.FULL_RESTORE(), 0, baseCost * 2.25)], [new ModifierTypeOption(modifierTypes.SACRED_ASH(), 0, baseCost * 10)], ]; - return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat(); + const opts = options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat(); + applyChallenges(ChallengeType.SHOP_MODIFY, opts); + return opts; } export function getEnemyBuffModifierForWave( diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts index 4107a9cf087..c8de6fddbc7 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -13,6 +13,8 @@ import i18next from "i18next"; import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase"; import type Pokemon from "#app/field/pokemon"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { BooleanHolder } from "#app/utils"; export enum LearnMoveType { /** For learning a move via level-up, evolution, or other non-item-based event */ @@ -48,6 +50,13 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { const move = allMoves[this.moveId]; const currentMoveset = pokemon.getMoveset(); + const canLearn = new BooleanHolder(true); + applyChallenges(ChallengeType.BAN_MOVE_LEARNING, pokemon, this.moveId, 0, canLearn); + + if (!canLearn.value) { + return this.end(); + } + // The game first checks if the Pokemon already has the move and ends the phase if it does. const hasMoveAlready = currentMoveset.some(m => m.moveId === move.id) && this.moveId !== Moves.SKETCH; if (hasMoveAlready) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index f8edaa56981..51fcae1f6df 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -50,6 +50,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; export class MovePhase extends BattlePhase { protected _pokemon: Pokemon; @@ -349,9 +350,11 @@ export class MovePhase extends BattlePhase { // "commit" to using the move, deducting PP. if (!this.ignorePp) { - const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); + const ppUsed = new NumberHolder(1 + this.getPpIncreaseFromPressure(targets)); - this.move.usePp(ppUsed); + applyChallenges(ChallengeType.MODIFY_PP_USE, this.pokemon, this.move.moveId, ppUsed); + + this.move.usePp(ppUsed.value); globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed)); } diff --git a/src/system/achv.ts b/src/system/achv.ts index bd8595b2f94..10b0888aa7e 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -541,7 +541,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 1 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GEN_TWO_VICTORY: new ChallengeAchv( @@ -554,7 +554,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 2 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GEN_THREE_VICTORY: new ChallengeAchv( @@ -567,7 +567,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 3 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GEN_FOUR_VICTORY: new ChallengeAchv( @@ -580,7 +580,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 4 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GEN_FIVE_VICTORY: new ChallengeAchv( @@ -593,7 +593,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 5 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GEN_SIX_VICTORY: new ChallengeAchv( @@ -606,7 +606,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 6 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GEN_SEVEN_VICTORY: new ChallengeAchv( @@ -619,7 +619,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 7 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GEN_EIGHT_VICTORY: new ChallengeAchv( @@ -632,7 +632,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 8 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GEN_NINE_VICTORY: new ChallengeAchv( @@ -645,7 +645,7 @@ export const achvs = { c instanceof SingleGenerationChallenge && c.value === 9 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_NORMAL: new ChallengeAchv( @@ -658,7 +658,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 1 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_FIGHTING: new ChallengeAchv( @@ -671,7 +671,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 2 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_FLYING: new ChallengeAchv( @@ -684,7 +684,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 3 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_POISON: new ChallengeAchv( @@ -697,7 +697,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 4 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GROUND: new ChallengeAchv( @@ -710,7 +710,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 5 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_ROCK: new ChallengeAchv( @@ -723,7 +723,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 6 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_BUG: new ChallengeAchv( @@ -736,7 +736,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 7 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GHOST: new ChallengeAchv( @@ -749,7 +749,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 8 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_STEEL: new ChallengeAchv( @@ -762,7 +762,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 9 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_FIRE: new ChallengeAchv( @@ -775,7 +775,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 10 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_WATER: new ChallengeAchv( @@ -788,7 +788,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 11 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_GRASS: new ChallengeAchv( @@ -801,7 +801,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 12 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_ELECTRIC: new ChallengeAchv( @@ -814,7 +814,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 13 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_PSYCHIC: new ChallengeAchv( @@ -827,7 +827,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 14 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_ICE: new ChallengeAchv( @@ -840,7 +840,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 15 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_DRAGON: new ChallengeAchv( @@ -853,7 +853,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 16 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_DARK: new ChallengeAchv( @@ -866,7 +866,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 17 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), MONO_FAIRY: new ChallengeAchv( @@ -879,7 +879,7 @@ export const achvs = { c instanceof SingleTypeChallenge && c.value === 18 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), FRESH_START: new ChallengeAchv( @@ -892,7 +892,7 @@ export const achvs = { c instanceof FreshStartChallenge && c.value > 0 && !globalScene.gameMode.challenges.some( - c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, + c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT, Challenges.METRONOME].includes(c.id) && c.value > 0, ), ), INVERSE_BATTLE: new ChallengeAchv( @@ -901,7 +901,10 @@ export const achvs = { "INVERSE_BATTLE.description", "inverse", 100, - c => c instanceof InverseBattleChallenge && c.value > 0, + c => + c instanceof InverseBattleChallenge && + c.value > 0 && + !globalScene.gameMode.challenges.some(c => c.id === Challenges.METRONOME && c.value > 0), ), FLIP_STATS: new ChallengeAchv( "FLIP_STATS", @@ -909,7 +912,10 @@ export const achvs = { "FLIP_STATS.description", "dubious_disc", 100, - c => c instanceof FlipStatChallenge && c.value > 0, + c => + c instanceof FlipStatChallenge && + c.value > 0 && + !globalScene.gameMode.challenges.some(c => c.id === Challenges.METRONOME && c.value > 0), ), FLIP_INVERSE: new ChallengeAchv( "FLIP_INVERSE", @@ -920,7 +926,8 @@ export const achvs = { c => c instanceof FlipStatChallenge && c.value > 0 && - globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0), + globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0) && + !globalScene.gameMode.challenges.some(c => c.id === Challenges.METRONOME && c.value > 0), ).setSecret(), BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(), }; diff --git a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index d4b0de30535..1321e91ec12 100644 --- a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -84,7 +84,7 @@ describe("Department Store Sale - Mystery Encounter", () => { describe("Option 1 - TM Shop", () => { it("should have the correct properties", () => { const option = DepartmentStoreSaleEncounter.options[0]; - expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); expect(option.dialogue).toBeDefined(); expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.1.label`,