diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 683fb48a9ba..a82ae3a1432 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -25,6 +25,8 @@ 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"; @@ -344,6 +346,84 @@ 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 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 true; + } + + /** + * 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; @@ -888,6 +968,123 @@ export class LowerStarterPointsChallenge extends Challenge { } } +/** + * Challenge stops pokemon from healing every 10th wave + */ +export class NoFreeHealsChallenge extends Challenge { + constructor() { + super(Challenges.NO_AUTO_HEAL, 1); + } + + applyNoHealPhase(applyHealPhase: BooleanHolder): boolean { + applyHealPhase.value = false; + return true; + } + + 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", + ]; + + 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 { + const moveBlacklist = [Moves.REVIVAL_BLESSING]; + moveCanBeUsed.value = !moveBlacklist.includes(move.moveId); + return true; + } + + 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; + } + + override applyShouldFuse(pokemon: Pokemon, pokemonToFuse: Pokemon, canFuse: BooleanHolder): boolean { + if (pokemon!.isFainted() || pokemonToFuse.isFainted()) { + canFuse.value = false; + } + return true; + } + + 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 { + const lastMystery = globalScene.lastMysteryEncounter?.encounterType; + if (lastMystery === undefined && !(waveIndex % 10 === 1)) { + canAddToParty.value = false; + } + if (!(waveIndex % 10 === 1) && !(!this.mysteryEncounterBlacklist.includes(lastMystery!) && waveIndex % 10 === 2)) { + canAddToParty.value = false; + } + return true; + } + + 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 @@ -1039,6 +1236,90 @@ 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 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; @@ -1087,6 +1368,30 @@ 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.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; } } }); @@ -1114,6 +1419,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"); } @@ -1127,6 +1438,9 @@ export function initChallenges() { new FreshStartChallenge(), new InverseBattleChallenge(), new FlipStatChallenge(), + new NoFreeHealsChallenge(), + new LimitedCatchChallenge(), + new HardcoreChallenge(), ); } diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index 7a1c9821e89..bf6d2f5aabe 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -22,6 +22,7 @@ import { EggTier } from "#enums/egg-type"; import { ModifierTier } from "#enums/modifier-tier"; import { modifierTypes } from "#app/data/data-lists"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/aTrainersTest"; @@ -34,6 +35,7 @@ const namespace = "mysteryEncounters/aTrainersTest"; export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.A_TRAINERS_TEST, ) + .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.ROGUE) .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withIntroSpriteConfigs([]) // These are set in onInit() diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 483c577e851..6706e0a62f4 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/custom-pokemon-data"; import { randSeedInt } from "#app/utils/common"; +import { Challenges } from "#enums/challenges"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounters/slumberingSnorlax"; @@ -43,6 +44,7 @@ const namespace = "mysteryEncounters/slumberingSnorlax"; export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.SLUMBERING_SNORLAX, ) + .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(15, 150) .withCatchAllowed(true) 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 e2c87d8c0ae..994f1f7a221 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -31,6 +31,7 @@ import i18next from "i18next"; import { ModifierTier } from "#enums/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theWinstrateChallenge"; @@ -43,6 +44,7 @@ const namespace = "mysteryEncounters/theWinstrateChallenge"; export const TheWinstrateChallengeEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.THE_WINSTRATE_CHALLENGE, ) + .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.ROGUE) .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withIntroSpriteConfigs([ diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 4671869a2ba..d67a5fdc964 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -37,6 +37,8 @@ import { CustomPokemonData } from "#app/data/custom-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 { ChallengeType, applyChallenges } from "#app/data/challenge"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; @@ -703,6 +705,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/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 964d66d352e..ad477b57c89 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2504,14 +2504,36 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { defScore *= 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], opponent, false, false, undefined, true), 0.25); } + atkScore *= 1.25; //give more value for the pokemon's typing + const moveset = this.moveset; + let moveAtkScoreLenght = 0; + for (const move of moveset) { + if (move.getMove().category === MoveCategory.SPECIAL || move.getMove().category === MoveCategory.PHYSICAL) { + atkScore += opponent.getAttackTypeEffectiveness(move.getMove().type, this, false, true, undefined, true); + moveAtkScoreLenght++; + } + } + atkScore = atkScore / (moveAtkScoreLenght + 1); //calculate the median for the attack score /** * Based on this Pokemon's HP ratio compared to that of the opponent. * This ratio is multiplied by 1.5 if this Pokemon outspeeds the opponent; * however, the final ratio cannot be higher than 1. */ - let hpDiffRatio = this.getHpRatio() + (1 - opponent.getHpRatio()); - if (outspeed) { - hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1); + const hpRatio = this.getHpRatio(); + const oppHpRatio = opponent.getHpRatio(); + const isDying = hpRatio <= 0.2; + let hpDiffRatio = hpRatio + (1 - oppHpRatio); + if (isDying && this.isActive(true)) { //It might be a sacrifice candidate if hp under 20% + const badMatchup = atkScore < 1.5 && defScore < 1.5; + if (!outspeed && badMatchup) { //It might not be a worthy sacrifice if it doesn't outspeed or doesn't do enough damage + hpDiffRatio *= 0.85; + } else { + hpDiffRatio = Math.min((1 - hpRatio) + (outspeed ? 0.2 : 0.1), 1); + } + } else if (outspeed) { + hpDiffRatio = Math.min(hpDiffRatio * 1.25, 1); + } else if (hpRatio > 0.2 && hpRatio <= 0.4) { //Might be considered to be switched because it's not in low enough health + hpDiffRatio = Math.min (hpDiffRatio * 0.5, 1); } return (atkScore + defScore) * hpDiffRatio; } @@ -3206,6 +3228,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 2c848c69e18..70f0abc820b 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,7 @@ 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"; const outputModifierData = false; const useMaxWeightForOutput = false; @@ -2632,10 +2634,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, @@ -2645,6 +2651,7 @@ function getModifierTypeOptionWithRetry( 0, allowLuckUpgrades, ); + applyChallenges(ChallengeType.RANDOM_ITEM_BLACKLIST, candidate, isValidForChallenge); } return candidate!; } @@ -2675,7 +2682,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 e11f2c07ce8..c4519e09acd 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -45,6 +45,7 @@ import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; import { applyAbAttrs, applyPostItemLostAbAttrs } 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"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -2446,8 +2447,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..84bc677f4c4 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -24,6 +24,8 @@ 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 { ChallengeType, applyChallenges } from "#app/data/challenge"; export class AttemptCapturePhase extends PokemonPhase { public readonly phaseName = "AttemptCapturePhase"; @@ -285,6 +287,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 e1bf4c2296c..7dc2e9d4622 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -2,6 +2,8 @@ import { globalScene } from "#app/global-scene"; import { applyPostBattleAbAttrs } 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, ChallengeType } from "#app/data/challenge"; export class BattleEndPhase extends BattlePhase { public readonly phaseName = "BattleEndPhase"; @@ -67,6 +69,16 @@ export class BattleEndPhase extends BattlePhase { for (const pokemon of globalScene.getPokemonAllowedInBattle()) { applyPostBattleAbAttrs("PostBattleAbAttr", pokemon, false, 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 d7264b4aff2..eea2a9d4822 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -218,7 +218,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..1a017293126 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -1,6 +1,7 @@ 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, ChallengeType } from "#app/data/challenge"; export class PartyHealPhase extends BattlePhase { public readonly phaseName = "PartyHealPhase"; @@ -15,18 +16,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); + 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; + applyChallenges(ChallengeType.PREVENT_REVIVE, isReviveActive); + 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..9ccc28d6bd7 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -8,6 +8,9 @@ import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/util import { globalScene } from "#app/global-scene"; import { timedEventManager } from "#app/global-event-manager"; +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 +40,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 +109,10 @@ export class VictoryPhase extends PokemonPhase { ); globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase"); } + if (!isHealPhaseActive.value) { + //Push shop instead of healing phase for NoHealChallenge + globalScene.pushPhase(new 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..68ee8f60c44 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -16,6 +16,8 @@ import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import Phaser from "phaser"; import type { PokeballType } from "#enums/pokeball"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { BooleanHolder } from "#app/utils/common"; export const SHOP_OPTIONS_ROW_LIMIT = 7; const SINGLE_SHOP_ROW_YOFFSET = 12; @@ -211,9 +213,16 @@ 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 shopTypeOptions = removeHealShop + ? [] + : 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;