[Challenge] Add Nuzlocke-related Challenges

Co-authored-by: =?UTF-8?q?Matilde=20Sim=C3=B5es?= <matilde.simoes@tecnico.ulisboa.pt>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Sirzento <sirzento@gmx.de>
This commit is contained in:
xsn34kzx 2025-07-31 21:31:23 -04:00
parent 1b8082a177
commit ef7437815a
13 changed files with 384 additions and 38 deletions

View File

@ -13,15 +13,16 @@ import { Challenges } from "#enums/challenges";
import { TypeColor, TypeShadow } from "#enums/color"; import { TypeColor, TypeShadow } from "#enums/color";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import type { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import type { MoveSourceType } from "#enums/move-source-type"; import type { MoveSourceType } from "#enums/move-source-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import type { Pokemon } from "#field/pokemon"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
import { Trainer } from "#field/trainer"; import { Trainer } from "#field/trainer";
import type { ModifierTypeOption } from "#modifiers/modifier-type";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import type { DexAttrProps, GameData } from "#system/game-data"; import type { DexAttrProps, GameData } from "#system/game-data";
import { BooleanHolder, type NumberHolder, randSeedItem } from "#utils/common"; import { BooleanHolder, type NumberHolder, randSeedItem } from "#utils/common";
@ -345,6 +346,75 @@ export abstract class Challenge {
applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) { applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) {
return false; return false;
} }
/**
* An apply function for PARTY_HEAL. Derived classes should alter this.
* @returns Whether party healing is enabled or not
*/
applyPartyHeal(): boolean {
return true;
}
/**
* An apply function for SHOP. Derived classes should alter this.
* @returns Whether the shop is or is not available after a wave
*/
applyShop() {
return true;
}
/**
* An apply function for POKEMON_ADD_TO_PARTY. Derived classes should alter this.
* @param _pokemon - The pokemon being caught
* @return Whether the pokemon can be added to the party or not
*/
applyPokemonAddToParty(_pokemon: EnemyPokemon): boolean {
return true;
}
/**
* An apply function for POKEMON_FUSION. Derived classes should alter this.
* @param _pokemon - The pokemon being checked
* @returns Whether the selected pokemon is allowed to fuse or not
*/
applyPokemonFusion(_pokemon: PlayerPokemon): boolean {
return false;
}
/**
* An apply function for POKEMON_MOVE. Derived classes should alter this.
* @param _moveId - The move being checked
* @returns Whether the move can be used or not
*/
applyPokemonMove(_moveId: MoveId): boolean {
return true;
}
/**
* An apply function for SHOP_ITEM. Derived classes should alter this.
* @param _shopItem - The item being checked
* @returns Whether the item should be added to the shop or not
*/
applyShopItem(_shopItem: ModifierTypeOption | null): boolean {
return true;
}
/**
* An apply function for WAVE_REWARD. Derived classes should alter this.
* @param _reward - The reward being checked
* @returns Whether the reward should be added to the reward options or not
*/
applyWaveReward(_reward: ModifierTypeOption | null): boolean {
return true;
}
/**
* An apply function for PREVENT_REVIVE. Derived classes should alter this.
* @returns Whether fainting is a permanent status or not
*/
applyPreventRevive(): boolean {
return false;
}
} }
type ChallengeCondition = (data: GameData) => boolean; type ChallengeCondition = (data: GameData) => boolean;
@ -889,6 +959,89 @@ export class LowerStarterPointsChallenge extends Challenge {
} }
} }
/**
* Implements a No Support challenge
*/
export class NoSupportChallenge extends Challenge {
// 1 is no_heal
// 2 is no_shop
// 3 is both
constructor() {
super(Challenges.NO_SUPPORT, 3);
}
override applyPartyHeal(): boolean {
return this.value === 2;
}
override applyShop(): boolean {
return this.value === 1;
}
static override loadChallenge(source: NoSupportChallenge | any): NoSupportChallenge {
const newChallenge = new NoSupportChallenge();
newChallenge.value = source.value;
newChallenge.severity = source.severity;
return newChallenge;
}
}
/**
* Implements a Limited Catch challenge
*/
export class LimitedCatchChallenge extends Challenge {
constructor() {
super(Challenges.LIMITED_CATCH, 1);
}
override applyPokemonAddToParty(pokemon: EnemyPokemon): boolean {
return pokemon.metWave % 10 === 1;
}
static override loadChallenge(source: LimitedCatchChallenge | any): LimitedCatchChallenge {
const newChallenge = new LimitedCatchChallenge();
newChallenge.value = source.value;
newChallenge.severity = source.severity;
return newChallenge;
}
}
/**
* Implements a Permanent Faint challenge
*/
export class PermanentFaintChallenge extends Challenge {
constructor() {
super(Challenges.PERMANENT_FAINT, 1);
}
override applyPokemonFusion(pokemon: PlayerPokemon): boolean {
return !pokemon.isFainted();
}
override applyShopItem(shopItem: ModifierTypeOption | null): boolean {
return shopItem?.type.group !== "revive";
}
override applyWaveReward(reward: ModifierTypeOption | null): boolean {
return this.applyShopItem(reward);
}
override applyPokemonMove(moveId: MoveId) {
return moveId !== MoveId.REVIVAL_BLESSING;
}
override applyPreventRevive(): boolean {
return true;
}
static override loadChallenge(source: PermanentFaintChallenge | any): PermanentFaintChallenge {
const newChallenge = new PermanentFaintChallenge();
newChallenge.value = source.value;
newChallenge.severity = source.severity;
return newChallenge;
}
}
/** /**
* Apply all challenges that modify starter choice. * Apply all challenges that modify starter choice.
* @param challengeType {@link ChallengeType} ChallengeType.STARTER_CHOICE * @param challengeType {@link ChallengeType} ChallengeType.STARTER_CHOICE
@ -1041,6 +1194,66 @@ export function applyChallenges(
export function applyChallenges(challengeType: ChallengeType.FLIP_STAT, pokemon: Pokemon, baseStats: number[]): boolean; export function applyChallenges(challengeType: ChallengeType.FLIP_STAT, pokemon: Pokemon, baseStats: number[]): boolean;
/**
* Apply all challenges that conditionally enable or disable automatic party healing during biome transitions
* @param challengeType - {@linkcode ChallengeType.PARTY_HEAL}
* @returns Whether party healing is enabled or not
*/
export function applyChallenges(challengeType: ChallengeType.PARTY_HEAL): boolean;
/**
* Apply all challenges that conditionally enable or disable the shop
* @returns Whether the shop is or is not available after a wave
*/
export function applyChallenges(challengeType: ChallengeType.SHOP): boolean;
/**
* Apply all challenges that validate whether a pokemon can be added to the player's party or not
* @param challengeType - {@linkcode ChallengeType.POKEMON_ADD_TO_PARTY}
* @param pokemon - The pokemon being caught
* @return Whether the pokemon can be added to the party or not
*/
export function applyChallenges(challengeType: ChallengeType.POKEMON_ADD_TO_PARTY, pokemon: EnemyPokemon): boolean;
/**
* Apply all challenges that validate whether a pokemon is allowed to fuse or not
* @param challengeType - {@linkcode ChallengeType.POKEMON_FUSION}
* @param pokemon - The pokemon being checked
* @returns Whether the selected pokemon is allowed to fuse or not
*/
export function applyChallenges(challengeType: ChallengeType.POKEMON_FUSION, pokemon: PlayerPokemon): boolean;
/**
* Apply all challenges that validate whether particular moves can or cannot be used
* @param challengeType - {@linkcode ChallengeType.POKEMON_MOVE}
* @param moveId - The move being checked
* @returns Whether the move can be used or not
*/
export function applyChallenges(challengeType: ChallengeType.POKEMON_MOVE, moveId: MoveId): boolean;
/**
* Apply all challenges that validate whether particular items are or are not sold in the shop
* @param challengeType - {@linkcode ChallengeType.SHOP_ITEM}
* @param shopItem - The item being checked
* @returns Whether the item should be added to the shop or not
*/
export function applyChallenges(challengeType: ChallengeType.SHOP_ITEM, shopItem: ModifierTypeOption | null): boolean;
/**
* Apply all challenges that validate whether particular items will be given as a reward after a wave
* @param challengeType - {@linkcode ChallengeType.WAVE_REWARD}
* @param reward - The reward being checked
* @returns Whether the reward should be added to the reward options or not
*/
export function applyChallenges(challengeType: ChallengeType.WAVE_REWARD, reward: ModifierTypeOption | null): boolean;
/**
* Apply all challenges that prevent recovery from fainting
* @param challengeType - {@linkcode ChallengeType.PREVENT_REVIVE}
* @returns Whether fainting is a permanent status or not
*/
export function applyChallenges(challengeType: ChallengeType.PREVENT_REVIVE): boolean;
export function applyChallenges(challengeType: ChallengeType, ...args: any[]): boolean { export function applyChallenges(challengeType: ChallengeType, ...args: any[]): boolean {
let ret = false; let ret = false;
globalScene.gameMode.challenges.forEach(c => { globalScene.gameMode.challenges.forEach(c => {
@ -1088,6 +1301,30 @@ export function applyChallenges(challengeType: ChallengeType, ...args: any[]): b
case ChallengeType.FLIP_STAT: case ChallengeType.FLIP_STAT:
ret ||= c.applyFlipStat(args[0], args[1]); ret ||= c.applyFlipStat(args[0], args[1]);
break; break;
case ChallengeType.PARTY_HEAL:
ret ||= c.applyPartyHeal();
break;
case ChallengeType.SHOP:
ret ||= c.applyShop();
break;
case ChallengeType.POKEMON_ADD_TO_PARTY:
ret ||= c.applyPokemonAddToParty(args[0]);
break;
case ChallengeType.POKEMON_FUSION:
ret ||= c.applyPokemonFusion(args[0]);
break;
case ChallengeType.POKEMON_MOVE:
ret ||= c.applyPokemonMove(args[0]);
break;
case ChallengeType.SHOP_ITEM:
ret ||= c.applyShopItem(args[0]);
break;
case ChallengeType.WAVE_REWARD:
ret ||= c.applyWaveReward(args[0]);
break;
case ChallengeType.PREVENT_REVIVE:
ret ||= c.applyPreventRevive();
break;
} }
} }
}); });
@ -1115,6 +1352,12 @@ export function copyChallenge(source: Challenge | any): Challenge {
return InverseBattleChallenge.loadChallenge(source); return InverseBattleChallenge.loadChallenge(source);
case Challenges.FLIP_STAT: case Challenges.FLIP_STAT:
return FlipStatChallenge.loadChallenge(source); return FlipStatChallenge.loadChallenge(source);
case Challenges.LIMITED_CATCH:
return LimitedCatchChallenge.loadChallenge(source);
case Challenges.NO_SUPPORT:
return NoSupportChallenge.loadChallenge(source);
case Challenges.PERMANENT_FAINT:
return PermanentFaintChallenge.loadChallenge(source);
} }
throw new Error("Unknown challenge copied"); throw new Error("Unknown challenge copied");
} }
@ -1128,6 +1371,9 @@ export function initChallenges() {
new FreshStartChallenge(), new FreshStartChallenge(),
new InverseBattleChallenge(), new InverseBattleChallenge(),
new FlipStatChallenge(), new FlipStatChallenge(),
new LimitedCatchChallenge(),
new NoSupportChallenge(),
new PermanentFaintChallenge(),
); );
} }

View File

@ -1,4 +1,6 @@
import { applyChallenges } from "#data/challenge";
import { allMoves } from "#data/data-lists"; import { allMoves } from "#data/data-lists";
import { ChallengeType } from "#enums/challenge-type";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type { Move } from "#moves/move"; import type { Move } from "#moves/move";
@ -46,15 +48,12 @@ export class PokemonMove {
*/ */
isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean { isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean {
// TODO: Add Sky Drop's 1 turn stall // TODO: Add Sky Drop's 1 turn stall
if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) { const isBattleRestricted = this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon);
return false; const hasPp = ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1;
} const isNotChallengeRestricted = !pokemon.isPlayer() || applyChallenges(ChallengeType.POKEMON_MOVE, this.moveId);
const isUnimplemented = this.getMove().name.endsWith(" (N)");
if (this.getMove().name.endsWith(" (N)")) { return !isBattleRestricted && hasPp && isNotChallengeRestricted && !isUnimplemented;
return false;
}
return ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1;
} }
getMove(): Move { getMove(): Move {

View File

@ -1,6 +1,7 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { speciesStarterCosts } from "#balance/starters"; import { speciesStarterCosts } from "#balance/starters";
import { applyChallenges } from "#data/challenge";
import { modifierTypes } from "#data/data-lists"; import { modifierTypes } from "#data/data-lists";
import { Gender } from "#data/gender"; import { Gender } from "#data/gender";
import { import {
@ -13,6 +14,7 @@ import { CustomPokemonData } from "#data/pokemon-data";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
import { getStatusEffectCatchRateMultiplier } from "#data/status-effect"; import { getStatusEffectCatchRateMultiplier } from "#data/status-effect";
import type { AbilityId } from "#enums/ability-id"; import type { AbilityId } from "#enums/ability-id";
import { ChallengeType } from "#enums/challenge-type";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import type { PokemonType } from "#enums/pokemon-type"; import type { PokemonType } from "#enums/pokemon-type";
@ -706,6 +708,12 @@ export async function catchPokemon(
}); });
}; };
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
// TODO: Address ME edge cases (Safari Zone, etc.)
if (!applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon)) {
removePokemon();
end();
return;
}
if (globalScene.getPlayerParty().length === 6) { if (globalScene.getPlayerParty().length === 6) {
const promptRelease = () => { const promptRelease = () => {
globalScene.ui.showText( globalScene.ui.showText(

View File

@ -65,5 +65,45 @@ export enum ChallengeType {
/** /**
* Modifies what the pokemon stats for Flip Stat Mode. * Modifies what the pokemon stats for Flip Stat Mode.
*/ */
FLIP_STAT FLIP_STAT,
/**
* Challenges which conditionally enable or disable automatic party healing during biome transitions
* @see {@linkcode Challenge.applyPartyHealAvailability}
*/
PARTY_HEAL,
/**
* Challenges which conditionally enable or disable the shop
* @see {@linkcode Challenge.applyShopAvailability}
*/
SHOP,
/**
* Challenges which validate whether a pokemon can be added to the player's party or not
* @see {@linkcode Challenge.applyCatchAvailability}
*/
POKEMON_ADD_TO_PARTY,
/**
* Challenges which validate whether a pokemon is allowed to fuse or not
* @see {@linkcode Challenge.applyFusionAvailability}
*/
POKEMON_FUSION,
/**
* Challenges which validate whether particular moves can or cannot be used
* @see {@linkcode Challenge.applyMoveAvailability}
*/
POKEMON_MOVE,
/**
* Challenges which validate whether particular items are or are not sold in the shop
* @see {@linkcode Challenge.applyShopItems}
*/
SHOP_ITEM,
/**
* Challenges which validate whether particular items will be given as a reward after a wave
* @see {@linkcode Challenge.applyWaveRewards}
*/
WAVE_REWARD,
/**
* Challenges which prevent recovery from fainting
* @see {@linkcode Challenge.applyPermanentFaint}
*/
PREVENT_REVIVE,
} }

View File

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

View File

@ -2,8 +2,7 @@ import { FixedBattleConfig } from "#app/battle";
import { CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import type { Challenge } from "#data/challenge"; import { allChallenges, applyChallenges, type Challenge, copyChallenge } from "#data/challenge";
import { allChallenges, applyChallenges, copyChallenge } from "#data/challenge";
import { getDailyStartingBiome } from "#data/daily-run"; import { getDailyStartingBiome } from "#data/daily-run";
import { allSpecies } from "#data/data-lists"; import { allSpecies } from "#data/data-lists";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
@ -311,6 +310,14 @@ export class GameMode implements GameModeConfig {
return this.battleConfig[waveIndex]; return this.battleConfig[waveIndex];
} }
/**
* Checks if the game mode has the shop enabled or not
* @returns Whether the shop is available or not
*/
getShopAvailability(): boolean {
return !this.hasNoShop && this.modeId === GameModes.CHALLENGE && applyChallenges(ChallengeType.SHOP);
}
getClearScoreBonus(): number { getClearScoreBonus(): number {
switch (this.modeId) { switch (this.modeId) {
case GameModes.CLASSIC: case GameModes.CLASSIC:

View File

@ -6,6 +6,7 @@ import Overrides from "#app/overrides";
import { EvolutionItem, pokemonEvolutions } from "#balance/pokemon-evolutions"; import { EvolutionItem, pokemonEvolutions } from "#balance/pokemon-evolutions";
import { tmPoolTiers, tmSpecies } from "#balance/tms"; import { tmPoolTiers, tmSpecies } from "#balance/tms";
import { getBerryEffectDescription, getBerryName } from "#data/berry"; import { getBerryEffectDescription, getBerryName } from "#data/berry";
import { applyChallenges } from "#data/challenge";
import { allMoves, modifierTypes } from "#data/data-lists"; import { allMoves, modifierTypes } from "#data/data-lists";
import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers";
import { getNatureName, getNatureStatMultiplier } from "#data/nature"; import { getNatureName, getNatureStatMultiplier } from "#data/nature";
@ -14,6 +15,7 @@ import { pokemonFormChanges, SpeciesFormChangeCondition } from "#data/pokemon-fo
import { getStatusEffectDescriptor } from "#data/status-effect"; import { getStatusEffectDescriptor } from "#data/status-effect";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { ChallengeType } from "#enums/challenge-type";
import { FormChangeItem } from "#enums/form-change-item"; import { FormChangeItem } from "#enums/form-change-item";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
@ -533,7 +535,7 @@ export class PokemonReviveModifierType extends PokemonHpRestoreModifierType {
); );
this.selectFilter = (pokemon: PlayerPokemon) => { this.selectFilter = (pokemon: PlayerPokemon) => {
if (pokemon.hp) { if (pokemon.hp || applyChallenges(ChallengeType.PREVENT_REVIVE)) {
return PartyUiHandler.NoEffectMessage; return PartyUiHandler.NoEffectMessage;
} }
return null; return null;
@ -1262,7 +1264,7 @@ export class FusePokemonModifierType extends PokemonModifierType {
iconImage, iconImage,
(_type, args) => new FusePokemonModifier(this, (args[0] as PlayerPokemon).id, (args[1] as PlayerPokemon).id), (_type, args) => new FusePokemonModifier(this, (args[0] as PlayerPokemon).id, (args[1] as PlayerPokemon).id),
(pokemon: PlayerPokemon) => { (pokemon: PlayerPokemon) => {
if (pokemon.isFusion()) { if (pokemon.isFusion() || !applyChallenges(ChallengeType.POKEMON_FUSION, pokemon)) {
return PartyUiHandler.NoEffectMessage; return PartyUiHandler.NoEffectMessage;
} }
return null; return null;
@ -2574,11 +2576,14 @@ function getModifierTypeOptionWithRetry(
): ModifierTypeOption { ): ModifierTypeOption {
allowLuckUpgrades = allowLuckUpgrades ?? true; allowLuckUpgrades = allowLuckUpgrades ?? true;
let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades);
let candidateValidity = applyChallenges(ChallengeType.WAVE_REWARD, candidate);
let r = 0; let r = 0;
while ( while (
existingOptions.length && (existingOptions.length &&
++r < retryCount && ++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) ||
!candidateValidity
) { ) {
candidate = getNewModifierTypeOption( candidate = getNewModifierTypeOption(
party, party,
@ -2588,6 +2593,7 @@ function getModifierTypeOptionWithRetry(
0, 0,
allowLuckUpgrades, allowLuckUpgrades,
); );
candidateValidity = applyChallenges(ChallengeType.WAVE_REWARD, candidate);
} }
return candidate!; return candidate!;
} }
@ -2648,7 +2654,11 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseC
[new ModifierTypeOption(modifierTypeInitObj.FULL_RESTORE(), 0, baseCost * 2.25)], [new ModifierTypeOption(modifierTypeInitObj.FULL_RESTORE(), 0, baseCost * 2.25)],
[new ModifierTypeOption(modifierTypeInitObj.SACRED_ASH(), 0, baseCost * 10)], [new ModifierTypeOption(modifierTypeInitObj.SACRED_ASH(), 0, baseCost * 10)],
]; ];
return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat();
return options
.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30))
.flat()
.filter(shopItem => applyChallenges(ChallengeType.SHOP_ITEM, shopItem));
} }
export function getEnemyBuffModifierForWave( export function getEnemyBuffModifierForWave(

View File

@ -2,6 +2,7 @@ import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { SubstituteTag } from "#data/battler-tags"; import { SubstituteTag } from "#data/battler-tags";
import { applyChallenges } from "#data/challenge";
import { Gender } from "#data/gender"; import { Gender } from "#data/gender";
import { import {
doPokeballBounceAnim, doPokeballBounceAnim,
@ -12,6 +13,7 @@ import {
} from "#data/pokeball"; } from "#data/pokeball";
import { getStatusEffectCatchRateMultiplier } from "#data/status-effect"; import { getStatusEffectCatchRateMultiplier } from "#data/status-effect";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { ChallengeType } from "#enums/challenge-type";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
@ -287,6 +289,11 @@ export class AttemptCapturePhase extends PokemonPhase {
}); });
}; };
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
if (!applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon)) {
removePokemon();
end();
return;
}
if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) {
const promptRelease = () => { const promptRelease = () => {
globalScene.ui.showText( globalScene.ui.showText(

View File

@ -4,12 +4,14 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { speciesStarterCosts } from "#balance/starters"; import { speciesStarterCosts } from "#balance/starters";
import type { EncoreTag } from "#data/battler-tags"; import type { EncoreTag } from "#data/battler-tags";
import { TrappedTag } from "#data/battler-tags"; import { TrappedTag } from "#data/battler-tags";
import { applyChallenges } from "#data/challenge";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { ChallengeType } from "#enums/challenge-type";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
@ -220,18 +222,31 @@ export class CommandPhase extends FieldPhase {
const move = playerPokemon.getMoveset()[cursor]; const move = playerPokemon.getMoveset()[cursor];
globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.setMode(UiMode.MESSAGE);
// Decides between a Disabled, Not Implemented, or No PP translation message // Set the translation key for why the move cannot be selected. The reasons can be:
const errorMessage = playerPokemon.isMoveRestricted(move.moveId, playerPokemon) // - If the move has been restricted in battle (e.g., Disable).
? playerPokemon // - If the move has no more PP.
.getRestrictingTag(move.moveId, playerPokemon)! // - If the move is restricted by a challenge.
.selectionDeniedText(playerPokemon, move.moveId) // - If the move is not implemented
: move.getName().endsWith(" (N)") let cannotUseKey: string;
? "battle:moveNotImplemented" if (playerPokemon.isMoveRestricted(move.moveId, playerPokemon)) {
: "battle:moveNoPP"; cannotUseKey = playerPokemon
.getRestrictingTag(move.moveId, playerPokemon)!
.selectionDeniedText(playerPokemon, move.moveId);
} else if (move.getPpRatio() === 0) {
cannotUseKey = "battle:moveNoPP";
} else if (!applyChallenges(ChallengeType.POKEMON_MOVE, move.moveId)) {
cannotUseKey = "battle:moveCannotUseChallenge";
} else if (move.getName().endsWith(" (N)")) {
cannotUseKey = "battle:moveNotImplemented";
} else {
// TODO: Consider a message that signals a being unusable for an unknown reason
cannotUseKey = "";
}
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
globalScene.ui.showText( globalScene.ui.showText(
i18next.t(errorMessage, { moveName: moveName }), i18next.t(cannotUseKey, { moveName: moveName }),
null, null,
() => { () => {
globalScene.ui.clearText(); globalScene.ui.clearText();

View File

@ -1,4 +1,6 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { applyChallenges } from "#data/challenge";
import { ChallengeType } from "#enums/challenge-type";
import { BattlePhase } from "#phases/battle-phase"; import { BattlePhase } from "#phases/battle-phase";
import { fixedInt } from "#utils/common"; import { fixedInt } from "#utils/common";
@ -20,13 +22,16 @@ export class PartyHealPhase extends BattlePhase {
globalScene.fadeOutBgm(1000, false); globalScene.fadeOutBgm(1000, false);
} }
globalScene.ui.fadeOut(1000).then(() => { globalScene.ui.fadeOut(1000).then(() => {
const preventRevive = applyChallenges(ChallengeType.PREVENT_REVIVE);
for (const pokemon of globalScene.getPlayerParty()) { for (const pokemon of globalScene.getPlayerParty()) {
pokemon.hp = pokemon.getMaxHp(); if (!(pokemon.isFainted() && preventRevive)) {
pokemon.resetStatus(true, false, false, true); pokemon.hp = pokemon.getMaxHp();
for (const move of pokemon.moveset) { pokemon.resetStatus(true, false, false, true);
move.ppUsed = 0; for (const move of pokemon.moveset) {
move.ppUsed = 0;
}
pokemon.updateInfo(true);
} }
pokemon.updateInfo(true);
} }
const healSong = globalScene.playSoundWithoutBgm("heal"); const healSong = globalScene.playSoundWithoutBgm("heal");
globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => { globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => {

View File

@ -1,6 +1,8 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { biomeLinks, getBiomeName } from "#balance/biomes"; import { biomeLinks, getBiomeName } from "#balance/biomes";
import { applyChallenges } from "#data/challenge";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { ChallengeType } from "#enums/challenge-type";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { MapModifier, MoneyInterestModifier } from "#modifiers/modifier"; import { MapModifier, MoneyInterestModifier } from "#modifiers/modifier";
import { BattlePhase } from "#phases/battle-phase"; import { BattlePhase } from "#phases/battle-phase";
@ -20,7 +22,9 @@ export class SelectBiomePhase extends BattlePhase {
const setNextBiome = (nextBiome: BiomeId) => { const setNextBiome = (nextBiome: BiomeId) => {
if (nextWaveIndex % 10 === 1) { if (nextWaveIndex % 10 === 1) {
globalScene.applyModifiers(MoneyInterestModifier, true); globalScene.applyModifiers(MoneyInterestModifier, true);
globalScene.phaseManager.unshiftNew("PartyHealPhase", false); if (applyChallenges(ChallengeType.PARTY_HEAL)) {
globalScene.phaseManager.unshiftNew("PartyHealPhase", false);
}
} }
globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome); globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome);
this.end(); this.end();

View File

@ -1,8 +1,10 @@
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { applyChallenges } from "#data/challenge";
import { modifierTypes } from "#data/data-lists"; import { modifierTypes } from "#data/data-lists";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { ChallengeType } from "#enums/challenge-type";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import type { CustomModifierSettings } from "#modifiers/modifier-type"; import type { CustomModifierSettings } from "#modifiers/modifier-type";
import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils"; import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils";
@ -63,7 +65,7 @@ export class VictoryPhase extends PokemonPhase {
break; break;
} }
} }
if (globalScene.currentBattle.waveIndex % 10) { if (globalScene.currentBattle.waveIndex % 10 !== 0 || !applyChallenges(ChallengeType.PARTY_HEAL)) {
globalScene.phaseManager.pushNew( globalScene.phaseManager.pushNew(
"SelectModifierPhase", "SelectModifierPhase",
undefined, undefined,

View File

@ -209,10 +209,10 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
this.updateRerollCostText(); this.updateRerollCostText();
const typeOptions = args[1] as ModifierTypeOption[]; const typeOptions = args[1] as ModifierTypeOption[];
const removeHealShop = globalScene.gameMode.hasNoShop; const hasShop = globalScene.gameMode.getShopAvailability();
const baseShopCost = new NumberHolder(globalScene.getWaveMoneyAmount(1)); const baseShopCost = new NumberHolder(globalScene.getWaveMoneyAmount(1));
globalScene.applyModifier(HealShopCostModifier, true, baseShopCost); globalScene.applyModifier(HealShopCostModifier, true, baseShopCost);
const shopTypeOptions = !removeHealShop const shopTypeOptions = hasShop
? getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value) ? getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value)
: []; : [];
const optionsYOffset = const optionsYOffset =
@ -370,7 +370,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
if (globalScene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) { if (globalScene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) {
this.setRowCursor(0); this.setRowCursor(0);
this.setCursor(2); this.setCursor(2);
} else if (globalScene.shopCursorTarget === ShopCursorTarget.SHOP && globalScene.gameMode.hasNoShop) { } else if (globalScene.shopCursorTarget === ShopCursorTarget.SHOP && !hasShop) {
this.setRowCursor(ShopCursorTarget.REWARDS); this.setRowCursor(ShopCursorTarget.REWARDS);
this.setCursor(0); this.setCursor(0);
} else { } else {