diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 3fdd83c185d..3b1d5ad33ca 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -1,4 +1,4 @@ -import { BooleanHolder, type NumberHolder, randSeedItem } from "#app/utils/common"; +import { BooleanHolder, type NumberHolder, randSeedItem, isNullOrUndefined } from "#app/utils/common"; import { deepCopy } from "#app/utils/data"; import i18next from "i18next"; import type { DexAttrProps, GameData } from "#app/system/game-data"; @@ -20,12 +20,14 @@ import { Challenges } from "#enums/challenges"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { Nature } from "#enums/nature"; -import type { MoveId } from "#enums/move-id"; +import { MoveId } from "#enums/move-id"; import { TypeColor, TypeShadow } from "#enums/color"; import { ModifierTier } from "#enums/modifier-tier"; import { globalScene } from "#app/global-scene"; import { pokemonFormChanges } from "./pokemon-forms"; import { pokemonEvolutions } from "./balance/pokemon-evolutions"; +import type { ModifierTypeOption } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { ChallengeType } from "#enums/challenge-type"; import type { MoveSourceType } from "#enums/move-source-type"; @@ -345,6 +347,93 @@ export abstract class Challenge { applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) { return false; } + + /** + * An apply function for NO_AUTO_HEAL challenges. Derived classes should alter this. + * @param _applyHealPhase {@link BooleanHolder} Whether it should apply the heal phase. + * @returns {@link boolean} if this function did anything. + */ + applyNoHealPhase(_applyHealPhase: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for NO_SHOP_PHASE. Derived classes should alter this. + * @param _applyShopPhase {@link BooleanHolder} Whether it should apply the shop phase. + * @returns {@link boolean} if this function did anything. + */ + applyNoShopPhase(_applyShopPhase: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for PREVENT_REVIVE. Derived classes should alter this. + * @param _canBeRevived {@link BooleanHolder} Whether it should revive the fainted Pokemon. + * @returns {@link boolean} if this function did anything. + */ + applyRevivePrevention(_canBeRevived: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for RANDOM_ITEM_BLACKLIST. Derived classes should alter this. + * @param _randomItem {@link ModifierTypeOption} The random item in question. + * @param _isValid {@link BooleanHolder} Whether it should load the random item. + * @returns {@link boolean} if this function did anything. + */ + applyRandomItemBlacklist(_randomItem: ModifierTypeOption | null, _isValid: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for SHOP_ITEM_BLACKLIST. Derived classes should alter this. + * @param _shopItem {@link ModifierTypeOption} The shop item in question. + * @param _isValid {@link BooleanHolder} Whether the shop should have the item. + * @returns {@link boolean} if this function did anything. + */ + applyShopItemBlacklist(_shopItem: ModifierTypeOption | null, _isValid: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for MOVE_BLACKLIST. Derived classes should alter this. + * @param _move {@link PokemonMove} The move in question. + * @param _isValid {@link BooleanHolder} Whether the move should be allowed. + * @returns {@link boolean} if this function did anything. + */ + applyMoveBlacklist(_move: PokemonMove, _isValid: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for DELETE_POKEMON. Derived classes should alter this. + * @param _canStay {@link BooleanHolder} Whether the pokemon can stay in team after death. + * @returns {@link boolean} if this function did anything. + */ + applyDeletePokemon(_canStay: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for ADD_POKEMON_TO_PARTY. Derived classes should alter this. + * @param _waveIndex {@link BooleanHolder} The current wave. + * @param _canAddToParty {@link BooleanHolder} Whether the pokemon can be caught. + * @returns {@link boolean} if this function did anything. + */ + applyAddPokemonToParty(_waveIndex: number, _canAddToParty: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for SHOULD_FUSE. Derived classes should alter this. + * @param _pokemon {@link Pokemon} The first chosen pokemon for fusion. + * @param _pokemonTofuse {@link Pokemon} The second chosen pokemon for fusion. + * @param _canFuse {@link BooleanHolder} Whether the pokemons can fuse. + * @returns {@link boolean} if this function did anything. + */ + applyShouldFuse(_pokemon: Pokemon, _pokemonTofuse: Pokemon, _canFuse: BooleanHolder): boolean { + return false; + } } type ChallengeCondition = (data: GameData) => boolean; @@ -889,6 +978,143 @@ export class LowerStarterPointsChallenge extends Challenge { } } +/** + * Challenge stops pokemon from healing every 10th wave + */ +export class NoFreeHealsChallenge extends Challenge { + constructor() { + super(Challenges.NO_AUTO_HEAL, 3); + } + + applyNoHealPhase(applyHealPhase: BooleanHolder): boolean { + if (this.value !== 1) { + applyHealPhase.value = false; + return true; + } + return false; + } + + applyNoShopPhase(applyShopPhase: BooleanHolder): boolean { + if (this.value !== 2) { + applyShopPhase.value = false; + return true; + } + return false; + } + + static loadChallenge(source: NoFreeHealsChallenge | any): NoFreeHealsChallenge { + const newChallenge = new NoFreeHealsChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + +/** + * Challenge that removes the ability to revive fallen pokemon + */ +export class HardcoreChallenge extends Challenge { + private itemBlackList = [ + "modifierType:ModifierType.REVIVE", + "modifierType:ModifierType.MAX_REVIVE", + "modifierType:ModifierType.SACRED_ASH", + "modifierType:ModifierType.REVIVER_SEED", + ]; + + private moveBlacklist = [MoveId.REVIVAL_BLESSING]; + + constructor() { + super(Challenges.HARDCORE, 2); + } + + applyRandomItemBlacklist(randomItem: ModifierTypeOption, isValid: BooleanHolder): boolean { + if (randomItem !== null) { + isValid.value = !this.itemBlackList.includes(randomItem.type.localeKey); + } + return true; + } + + applyShopItemBlacklist(shopItem: ModifierTypeOption, isValid: BooleanHolder): boolean { + isValid.value = !this.itemBlackList.includes(shopItem.type.localeKey); + return true; + } + + applyMoveBlacklist(move: PokemonMove, moveCanBeUsed: BooleanHolder): boolean { + if (this.moveBlacklist.includes(move.moveId)) { + moveCanBeUsed.value = false; + return true; + } + return false; + } + + applyRevivePrevention(canBeRevived: BooleanHolder): boolean { + canBeRevived.value = false; + return true; + } + + applyDeletePokemon(canStay: BooleanHolder): boolean { + if (this.value === 2) { + canStay.value = false; + } else { + canStay.value = true; + return true; + } + return false; + } + + override applyShouldFuse(pokemon: Pokemon, pokemonToFuse: Pokemon, canFuse: BooleanHolder): boolean { + if (pokemon!.isFainted() || pokemonToFuse.isFainted()) { + canFuse.value = false; + return true; + } + return false; + } + + static override loadChallenge(source: HardcoreChallenge | any): HardcoreChallenge { + const newChallenge = new HardcoreChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + +/** + * Challenge that limits the amount of caught pokemons by 1 per biome stage + */ +export class LimitedCatchChallenge extends Challenge { + private mysteryEncounterBlacklist = [ + MysteryEncounterType.ABSOLUTE_AVARICE, + MysteryEncounterType.DANCING_LESSONS, + MysteryEncounterType.SAFARI_ZONE, + MysteryEncounterType.THE_POKEMON_SALESMAN, + MysteryEncounterType.UNCOMMON_BREED, + ]; + constructor() { + super(Challenges.LIMITED_CATCH, 1); + } + + override applyAddPokemonToParty(waveIndex: number, canAddToParty: BooleanHolder): boolean { + if (waveIndex % 10 !== 1) { + const lastMystery = globalScene.lastMysteryEncounter?.encounterType; + if ( + isNullOrUndefined(lastMystery) || + !(waveIndex % 10 === 2 && !this.mysteryEncounterBlacklist.includes(lastMystery!)) + ) { + canAddToParty.value = false; + return true; + } + } + return false; + } + + static override loadChallenge(source: LimitedCatchChallenge | any): LimitedCatchChallenge { + const newChallenge = new LimitedCatchChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + /** * Apply all challenges that modify starter choice. * @param challengeType {@link ChallengeType} ChallengeType.STARTER_CHOICE @@ -1040,6 +1266,98 @@ export function applyChallenges( ): boolean; export function applyChallenges(challengeType: ChallengeType.FLIP_STAT, pokemon: Pokemon, baseStats: number[]): boolean; +/** + * Apply all challenges that modify whether a pokemon can be auto healed or not in wave 10m. + * @param challengeType {@link ChallengeType} ChallengeType.NO_HEAL_PHASE + * @param applyHealPhase {@link BooleanHolder} Whether it should apply the heal phase. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.NO_HEAL_PHASE, applyHealPhase: BooleanHolder): boolean; + +/** + * Apply all challenges that modify whether the shop will appear. + * @param challengeType {@link ChallengeType} ChallengeType.NO_SHOP_PHASE + * @param applyShopPhase {@link BooleanHolder} Whether it should apply the shop phase. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.NO_SHOP_PHASE, applyShopPhase: BooleanHolder): boolean; +/** + * Apply all challenges that modify whether a shop item should be blacklisted. + * @param challengeType {@link ChallengeType} ChallengeType.SHOP_ITEM_BLACKLIST + * @param shopItem {@link ModifierTypeOption} The shop item in question. + * @param isValid {@link BooleanHolder} Whether the shop should have the item. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.SHOP_ITEM_BLACKLIST, + shopItem: ModifierTypeOption | null, + isValid: BooleanHolder, +): boolean; + +/** + * Apply all challenges that modify whether a reward item should be blacklisted. + * @param challengeType {@link ChallengeType} ChallengeType.RANDOM_ITEM_BLACKLIST + * @param randomItem {@link ModifierTypeOption} The random item in question. + * @param isValid {@link BooleanHolder} Whether it should load the random item. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.RANDOM_ITEM_BLACKLIST, + randomItem: ModifierTypeOption | null, + isValid: BooleanHolder, +): boolean; +/** + * Apply all challenges that modify whether a pokemon move should be blacklisted. + * @param challengeType {@link ChallengeType} ChallengeType.MOVE_BLACKLIST + * @param move {@link PokemonMove} The move in question. + * @param isValid {@link BooleanHolder} Whether the move should be allowed. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.MOVE_BLACKLIST, + move: PokemonMove, + isValid: BooleanHolder, +): boolean; +/** + * Apply all challenges that modify whether a pokemon should be removed from the team. + * @param challengeType {@link ChallengeType} ChallengeType.DELETE_POKEMON + * @param canStay {@link BooleanHolder} Whether the pokemon can stay in team after death. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.DELETE_POKEMON, canStay: BooleanHolder): boolean; +/** + * Apply all challenges that modify whether a pokemon should revive. + * @param challengeType {@link ChallengeType} ChallengeType.PREVENT_REVIVE + * @param canBeRevived {@link BooleanHolder} Whether it should revive the fainted Pokemon. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.PREVENT_REVIVE, canBeRevived: BooleanHolder): boolean; +/** + * Apply all challenges that modify whether a pokemon can be caught. + * @param challengeType {@link ChallengeType} ChallengeType.ADD_POKEMON_TO_PARTY + * @param waveIndex {@link BooleanHolder} The current wave. + * @param canAddToParty {@link BooleanHolder} Whether the pokemon can be caught. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.ADD_POKEMON_TO_PARTY, + waveIndex: number, + canAddToParty: BooleanHolder, +): boolean; +/** + * Apply all challenges that modify whether a pokemon can fuse. + * @param challengeType {@link ChallengeType} ChallengeType.SHOULD_FUSE + * @param pokemon {@link Pokemon} The first chosen pokemon for fusion. + * @param pokemonTofuse {@link Pokemon} The second chosen pokemon for fusion. + * @param canFuse {@link BooleanHolder} Whether the pokemons can fuse. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.SHOULD_FUSE, + pokemon: Pokemon, + pokemonTofuse: Pokemon, + canFuse: BooleanHolder, +): boolean; export function applyChallenges(challengeType: ChallengeType, ...args: any[]): boolean { let ret = false; @@ -1088,6 +1406,33 @@ export function applyChallenges(challengeType: ChallengeType, ...args: any[]): b case ChallengeType.FLIP_STAT: ret ||= c.applyFlipStat(args[0], args[1]); break; + case ChallengeType.NO_HEAL_PHASE: + ret ||= c.applyNoHealPhase(args[0]); + break; + case ChallengeType.NO_SHOP_PHASE: + ret ||= c.applyNoShopPhase(args[0]); + break; + case ChallengeType.SHOP_ITEM_BLACKLIST: + ret ||= c.applyShopItemBlacklist(args[0], args[1]); + break; + case ChallengeType.RANDOM_ITEM_BLACKLIST: + ret ||= c.applyRandomItemBlacklist(args[0], args[1]); + break; + case ChallengeType.MOVE_BLACKLIST: + ret ||= c.applyMoveBlacklist(args[0], args[1]); + break; + case ChallengeType.DELETE_POKEMON: + ret ||= c.applyDeletePokemon(args[0]); + break; + case ChallengeType.PREVENT_REVIVE: + ret ||= c.applyRevivePrevention(args[0]); + break; + case ChallengeType.ADD_POKEMON_TO_PARTY: + ret ||= c.applyAddPokemonToParty(args[0], args[1]); + break; + case ChallengeType.SHOULD_FUSE: + ret ||= c.applyShouldFuse(args[0], args[1], args[2]); + break; } } }); @@ -1115,6 +1460,12 @@ export function copyChallenge(source: Challenge | any): Challenge { return InverseBattleChallenge.loadChallenge(source); case Challenges.FLIP_STAT: return FlipStatChallenge.loadChallenge(source); + case Challenges.NO_AUTO_HEAL: + return NoFreeHealsChallenge.loadChallenge(source); + case Challenges.HARDCORE: + return HardcoreChallenge.loadChallenge(source); + case Challenges.LIMITED_CATCH: + return LimitedCatchChallenge.loadChallenge(source); } throw new Error("Unknown challenge copied"); } @@ -1128,6 +1479,9 @@ export function initChallenges() { new FreshStartChallenge(), new InverseBattleChallenge(), new FlipStatChallenge(), + new NoFreeHealsChallenge(), + new LimitedCatchChallenge(), + new HardcoreChallenge(), ); } diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index d6a85dee119..e4771ebe8b8 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -31,6 +31,7 @@ import { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; import { randSeedInt } from "#app/utils/common"; +import { Challenges } from "#enums/challenges"; import { MoveUseMode } from "#enums/move-use-mode"; /** i18n namespace for the encounter */ diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index f3655217b5a..a312a2bf44f 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -37,6 +37,9 @@ import { CustomPokemonData } from "#app/data/pokemon/pokemon-data"; import type { AbilityId } from "#enums/ability-id"; import type { PokeballType } from "#enums/pokeball"; import { StatusEffect } from "#enums/status-effect"; +import { BooleanHolder } from "#app/utils/common"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; @@ -703,6 +706,17 @@ export async function catchPokemon( }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { + const challengeCanAddToParty = new BooleanHolder(true); + applyChallenges( + ChallengeType.ADD_POKEMON_TO_PARTY, + globalScene.currentBattle.waveIndex, + challengeCanAddToParty, + ); + if (!challengeCanAddToParty.value) { + removePokemon(); + end(); + return; + } if (globalScene.getPlayerParty().length === 6) { const promptRelease = () => { globalScene.ui.showText( diff --git a/src/enums/challenge-type.ts b/src/enums/challenge-type.ts index d9b1fce3e6e..19bcd1fb172 100644 --- a/src/enums/challenge-type.ts +++ b/src/enums/challenge-type.ts @@ -65,5 +65,48 @@ export enum ChallengeType { /** * Modifies what the pokemon stats for Flip Stat Mode. */ - FLIP_STAT + FLIP_STAT, + /** + * Challenge that modifies if the player should auto heal every 10th wave + */ + NO_HEAL_PHASE, + /** + * Challenge that modifies if the shop should appear + */ + NO_SHOP_PHASE, + /** + * Modifies if the shop item is blacklisted + * @see {@linkcode Challenge.applyShopItemBlacklist} + */ + SHOP_ITEM_BLACKLIST, + /** + * Modifies if the random item is blacklisted + * @see {@linkcode Challenge.applyRandomItemBlacklist} + */ + RANDOM_ITEM_BLACKLIST, + /** + * Modifies if the move is blacklisted + * @see {@linkcode Challenge.applyMoveBlacklist} + */ + MOVE_BLACKLIST, + /** + * Modifies if pokemon are allowed to be revived from fainting + * @see {@linkcode Challenge.applyRevivePrevention} + */ + PREVENT_REVIVE, + /** + * Modifies if pokemon are allowed to be revived from fainting + * @see {@linkcode Challenge.applyDeletePokemon} + */ + DELETE_POKEMON, + /** + * Challenge that modifies if the player should catch pokemon on waves other than the first + * @see {@linkcode Challenge.applyAddPokemonToParty} + */ + ADD_POKEMON_TO_PARTY, + /** + * Modifies if pokemon are allowed to fuse + * @see {@linkcode Challenge.applyShouldFuse} + */ + SHOULD_FUSE, } diff --git a/src/enums/challenges.ts b/src/enums/challenges.ts index 7b506a61a2f..5d9edcefbee 100644 --- a/src/enums/challenges.ts +++ b/src/enums/challenges.ts @@ -6,4 +6,7 @@ export enum Challenges { FRESH_START, INVERSE_BATTLE, FLIP_STAT, + NO_AUTO_HEAL, + HARDCORE, + LIMITED_CATCH, } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0a8e8469115..a84f09c5875 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3231,6 +3231,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public trySelectMove(moveIndex: number, ignorePp?: boolean): boolean { const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null; + if (move !== null) { + const isValid = new BooleanHolder(true); + applyChallenges(ChallengeType.MOVE_BLACKLIST, move!, isValid); + if (!isValid.value) { + return false; + } + } return move?.isUsable(this, ignorePp) ?? false; } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index fcbe6b66a4e..76a97325331 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -111,6 +111,7 @@ import { NumberHolder, padInt, randSeedInt, + BooleanHolder, } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -128,6 +129,8 @@ import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; import { ModifierPoolType } from "#enums/modifier-pool-type"; import { getModifierPoolForType, getModifierType } from "#app/utils/modifier-utils"; import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; const outputModifierData = false; const useMaxWeightForOutput = false; @@ -2617,10 +2620,14 @@ function getModifierTypeOptionWithRetry( allowLuckUpgrades = allowLuckUpgrades ?? true; let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); let r = 0; + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(ChallengeType.RANDOM_ITEM_BLACKLIST, candidate, isValidForChallenge); while ( - existingOptions.length && - ++r < retryCount && - existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length + (existingOptions.length && + ++r < retryCount && + existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group) + .length) || + !isValidForChallenge.value ) { candidate = getNewModifierTypeOption( party, @@ -2630,6 +2637,7 @@ function getModifierTypeOptionWithRetry( 0, allowLuckUpgrades, ); + applyChallenges(ChallengeType.RANDOM_ITEM_BLACKLIST, candidate, isValidForChallenge); } return candidate!; } @@ -2660,7 +2668,9 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[], } export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseCost: number): ModifierTypeOption[] { - if (!(waveIndex % 10)) { + const isHealPhaseActive = new BooleanHolder(true); + applyChallenges(ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); + if (!(waveIndex % 10) && isHealPhaseActive.value) { return []; } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 247b64ca2c0..d261d2508aa 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -45,6 +45,8 @@ import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import type { ModifierInstanceMap, ModifierString } from "#app/@types/modifier-types"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -2426,8 +2428,13 @@ export class FusePokemonModifier extends ConsumablePokemonModifier { * @returns `true` if {@linkcode FusePokemonModifier} should be applied */ override shouldApply(playerPokemon?: PlayerPokemon, playerPokemon2?: PlayerPokemon): boolean { + const shouldFuse = new BooleanHolder(true); + applyChallenges(ChallengeType.SHOULD_FUSE, playerPokemon!, playerPokemon2!, shouldFuse); return ( - super.shouldApply(playerPokemon, playerPokemon2) && !!playerPokemon2 && this.fusePokemonId === playerPokemon2.id + super.shouldApply(playerPokemon, playerPokemon2) && + !!playerPokemon2 && + this.fusePokemonId === playerPokemon2.id && + shouldFuse.value ); } diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index f4e6725935a..13e0437ae9f 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -24,6 +24,9 @@ import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; import { globalScene } from "#app/global-scene"; import { Gender } from "#app/data/gender"; +import { BooleanHolder } from "#app/utils/common"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; export class AttemptCapturePhase extends PokemonPhase { public readonly phaseName = "AttemptCapturePhase"; @@ -285,6 +288,17 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { + const challengeCanAddToParty = new BooleanHolder(true); + applyChallenges( + ChallengeType.ADD_POKEMON_TO_PARTY, + globalScene.currentBattle.waveIndex, + challengeCanAddToParty, + ); + if (!challengeCanAddToParty.value) { + removePokemon(); + end(); + return; + } if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { const promptRelease = () => { globalScene.ui.showText( diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 297e20cb445..622800cc1d9 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -2,6 +2,9 @@ import { globalScene } from "#app/global-scene"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; +import { BooleanHolder } from "#app/utils/common"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; export class BattleEndPhase extends BattlePhase { public readonly phaseName = "BattleEndPhase"; @@ -67,6 +70,16 @@ export class BattleEndPhase extends BattlePhase { for (const pokemon of globalScene.getPokemonAllowedInBattle()) { applyAbAttrs("PostBattleAbAttr", { pokemon, victory: this.isVictory }); } + const canStay = new BooleanHolder(true); + applyChallenges(ChallengeType.DELETE_POKEMON, canStay); + if (!canStay.value) { + const party = globalScene.getPlayerParty().slice(); + for (const pokemon of party) { + if (pokemon.isFainted()) { + globalScene.removePokemonFromPlayerParty(pokemon); + } + } + } if (globalScene.currentBattle.moneyScattered) { globalScene.currentBattle.pickUpScatteredMoney(); diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 8281019b3c4..1203750ed55 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -227,7 +227,9 @@ export class CommandPhase extends FieldPhase { .selectionDeniedText(playerPokemon, move.moveId) : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" - : "battle:moveNoPP"; + : move.getPpRatio() + ? "battle:moveDisabled" + : "battle:moveNoPP"; const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator globalScene.ui.showText( diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index 765c7dbad8e..e93a5b642b9 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -1,6 +1,8 @@ import { globalScene } from "#app/global-scene"; -import { fixedInt } from "#app/utils/common"; +import { BooleanHolder, fixedInt } from "#app/utils/common"; import { BattlePhase } from "./battle-phase"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; export class PartyHealPhase extends BattlePhase { public readonly phaseName = "PartyHealPhase"; @@ -15,18 +17,27 @@ export class PartyHealPhase extends BattlePhase { start() { super.start(); + const isHealPhaseActive = new BooleanHolder(true); + const isReviveActive = new BooleanHolder(true); + applyChallenges(ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); + applyChallenges(ChallengeType.PREVENT_REVIVE, isReviveActive); + if (!isHealPhaseActive.value) { + return this.end(); + } const bgmPlaying = globalScene.isBgmPlaying(); if (bgmPlaying) { globalScene.fadeOutBgm(1000, false); } globalScene.ui.fadeOut(1000).then(() => { for (const pokemon of globalScene.getPlayerParty()) { - pokemon.hp = pokemon.getMaxHp(); - pokemon.resetStatus(true, false, false, true); - for (const move of pokemon.moveset) { - move.ppUsed = 0; + if (isReviveActive.value || !pokemon.isFainted()) { + pokemon.hp = pokemon.getMaxHp(); + pokemon.resetStatus(true, false, false, true); + for (const move of pokemon.moveset) { + move.ppUsed = 0; + } + pokemon.updateInfo(true); } - pokemon.updateInfo(true); } const healSong = globalScene.playSoundWithoutBgm("heal"); globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => { diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index ae5b727c2a6..fa54b80558e 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -8,6 +8,10 @@ import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/util import { globalScene } from "#app/global-scene"; import { timedEventManager } from "#app/global-event-manager"; +import { ChallengeType } from "#enums/challenge-type"; +import { BooleanHolder } from "#app/utils/common"; +import { applyChallenges } from "#app/data/challenge"; + export class VictoryPhase extends PokemonPhase { public readonly phaseName = "VictoryPhase"; /** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */ @@ -37,6 +41,8 @@ export class VictoryPhase extends PokemonPhase { return this.end(); } + const isHealPhaseActive = new BooleanHolder(true); + applyChallenges(ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); if ( !globalScene .getEnemyParty() @@ -104,6 +110,15 @@ export class VictoryPhase extends PokemonPhase { ); globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase"); } + if (!isHealPhaseActive.value) { + //Push shop instead of healing phase for NoHealChallenge + globalScene.phaseManager.pushNew( + "SelectModifierPhase", + undefined, + undefined, + this.getFixedBattleCustomModifiers(), + ); + } } if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) { diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 7f5bf997f88..fe4921155eb 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -16,6 +16,9 @@ import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import Phaser from "phaser"; import type { PokeballType } from "#enums/pokeball"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; +import { BooleanHolder } from "#app/utils/common"; export const SHOP_OPTIONS_ROW_LIMIT = 7; const SINGLE_SHOP_ROW_YOFFSET = 12; @@ -211,9 +214,19 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { const removeHealShop = globalScene.gameMode.hasNoShop; const baseShopCost = new NumberHolder(globalScene.getWaveMoneyAmount(1)); globalScene.applyModifier(HealShopCostModifier, true, baseShopCost); - const shopTypeOptions = !removeHealShop - ? getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value) - : []; + const isShopActive = new BooleanHolder(true); + applyChallenges(ChallengeType.NO_SHOP_PHASE, isShopActive); + const shopTypeOptions = + removeHealShop || !isShopActive.value + ? [] + : getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value).filter( + shopItem => { + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(ChallengeType.SHOP_ITEM_BLACKLIST, shopItem, isValidForChallenge); + return isValidForChallenge.value; + }, + ); + const optionsYOffset = shopTypeOptions.length > SHOP_OPTIONS_ROW_LIMIT ? -SINGLE_SHOP_ROW_YOFFSET : -DOUBLE_SHOP_ROW_YOFFSET;