Implement NuzLocke related challenges and AI changes

The Nuzlocke challenges that were implemented were the following:
- "No Free Heal" which consists in disabling the auto heal
that occurs every 10th wave, replacing it with a normal shop phase.
Changes made:
   - Added a new challenge "NO_AUTO_HEAL" with a challenge type
"NO_HEAL_PHASE" and created a new function "applyNoHealPhase"
to determine whether the Pokémon can heal if the challenge
is active ("challenge.ts").
   - Added a confirmation on the healing phase to check if the challenge
is active or not, and if it is, it should skip the phase
("party-heal-phase.ts").
- Changed the logistic of when the shop should be displayed, so
when the challenge is active the shop appears every 10th wave
("modifier-type.ts") and actually push the shop phase
("victory-phase.ts").
- "Hardcore": Challenge divided into two modes, normal and hard,
where fainted Pokémon can't be revived, in addition, the hard mode
deletes the fainted Pokémon so the player can't switch it's items
after death.
Changes made:
- Added a new challenge "HARDCORE" with several challenge types with the
correspondent apply functions ("challenge.ts"), each one
is used as follows:
	- RANDOM_ITEM_BLACKLIST: filter the reward items with only the valid
one's ("modifier-type.ts").
	- SHOP_ITEM_BLACKLIST: filter the shop items with only the valid
one's ("modifier-select-ui-handler.ts").
	- MOVE_BLACKLIST: checks if the move selected is allowed and if not
sends a message of no apply ("pokemon.ts").
	- DELETE_POKEMON: if hard mode was selected, automatically delete
the fainted Pokémon from the party ("battle-end-pahse.ts").
	- SHOULD_FUSE: changed the logic of should apply function to
prohibit the fusion with dead Pokémon ("modifier.ts").
	- PREVENT_REVIVE: prevent the gain of hp of fainted Pokémon
("party-heal-phase.ts").
- "Limited Catch": Only the first wild Pokémon encounter of
every biome can be added to the player's current party.
Changes made:
- Added a new challenge LIMITED_CATCH with a challenge type
 ADD_POKEMON_TO_PARTY and created a new function
"applyAddPokemonToParty" to determine whether the Pokémon can be
added to the party, which should
only occur every 11th wave if it isn't a catchable mystery encounter
or every 12th wave if the 11th wave was a catchable mystery encounter
("challenge.ts").
- Changed the logistic of adding a Pokémon where it can be
caught so that the "pokedex" is updated but the Pokémon isn't added
to the party of the player affecting specifically mystery encounters
("encounter-pokemon-utils.ts") and added the same logic to normal
encounters. ("attempt-capture-phase.ts")

The changes in the game AI were as follows ("pokemon.ts"):
- More accurately accounts for the Pokémon's actual moves and their
 effectiveness against the player instead of only the pokemon type
- Introduced logic to decide when a Pokémon should be sacrificed or
switched based on its HP and speed.

Signed-off-by: Matilde Simões <matilde.simoes@tecnico.ulisboa.pt>
Co-authored-by: Fuad Ali <fuad.ali@tecnico.ulisboa.pt>
This commit is contained in:
Matilde Simões 2025-06-04 20:33:03 +01:00 committed by mati-soda
parent ff9aefb0e5
commit 4afb9b695d
15 changed files with 453 additions and 18 deletions

View File

@ -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(),
);
}

View File

@ -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()

View File

@ -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)

View File

@ -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([

View File

@ -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(

View File

@ -6,4 +6,7 @@ export enum Challenges {
FRESH_START,
INVERSE_BATTLE,
FLIP_STAT,
NO_AUTO_HEAL,
HARDCORE,
LIMITED_CATCH,
}

View File

@ -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;
}

View File

@ -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 &&
(existingOptions.length &&
++r < retryCount &&
existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length
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 [];
}

View File

@ -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
);
}

View File

@ -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(

View File

@ -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();

View File

@ -218,6 +218,8 @@ export class CommandPhase extends FieldPhase {
.selectionDeniedText(playerPokemon, move.moveId)
: move.getName().endsWith(" (N)")
? "battle:moveNotImplemented"
: move.getPpRatio()
? "battle:moveDisabled"
: "battle:moveNoPP";
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator

View File

@ -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,12 +16,20 @@ 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()) {
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) {
@ -28,6 +37,7 @@ export class PartyHealPhase extends BattlePhase {
}
pokemon.updateInfo(true);
}
}
const healSong = globalScene.playSoundWithoutBgm("heal");
globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => {
healSong.destroy();

View File

@ -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()) {

View File

@ -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;