diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 938ee482d01..d3e28a75a7f 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -13,15 +13,16 @@ import { Challenges } from "#enums/challenges"; import { TypeColor, TypeShadow } from "#enums/color"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; 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 { Nature } from "#enums/nature"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; 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 type { ModifierTypeOption } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; import type { DexAttrProps, GameData } from "#system/game-data"; import { BooleanHolder, type NumberHolder, randSeedItem } from "#utils/common"; @@ -345,6 +346,75 @@ export abstract class Challenge { applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) { 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; @@ -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. * @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; +/** + * 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 { let ret = false; globalScene.gameMode.challenges.forEach(c => { @@ -1088,6 +1301,30 @@ export function applyChallenges(challengeType: ChallengeType, ...args: any[]): b case ChallengeType.FLIP_STAT: ret ||= c.applyFlipStat(args[0], args[1]); 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); case Challenges.FLIP_STAT: 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"); } @@ -1128,6 +1371,9 @@ export function initChallenges() { new FreshStartChallenge(), new InverseBattleChallenge(), new FlipStatChallenge(), + new LimitedCatchChallenge(), + new NoSupportChallenge(), + new PermanentFaintChallenge(), ); } diff --git a/src/data/moves/pokemon-move.ts b/src/data/moves/pokemon-move.ts index d3f68fe9db4..96966f241c8 100644 --- a/src/data/moves/pokemon-move.ts +++ b/src/data/moves/pokemon-move.ts @@ -1,4 +1,6 @@ +import { applyChallenges } from "#data/challenge"; import { allMoves } from "#data/data-lists"; +import { ChallengeType } from "#enums/challenge-type"; import type { MoveId } from "#enums/move-id"; import type { Pokemon } from "#field/pokemon"; import type { Move } from "#moves/move"; @@ -46,15 +48,12 @@ export class PokemonMove { */ isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean { // TODO: Add Sky Drop's 1 turn stall - if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) { - return false; - } + const isBattleRestricted = this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon); + 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 false; - } - - return ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1; + return !isBattleRestricted && hasPp && isNotChallengeRestricted && !isUnimplemented; } getMove(): Move { diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 19f06707257..72a418e5e7a 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { speciesStarterCosts } from "#balance/starters"; +import { applyChallenges } from "#data/challenge"; import { modifierTypes } from "#data/data-lists"; import { Gender } from "#data/gender"; import { @@ -13,6 +14,7 @@ import { CustomPokemonData } from "#data/pokemon-data"; import type { PokemonSpecies } from "#data/pokemon-species"; import { getStatusEffectCatchRateMultiplier } from "#data/status-effect"; import type { AbilityId } from "#enums/ability-id"; +import { ChallengeType } from "#enums/challenge-type"; import { PlayerGender } from "#enums/player-gender"; import type { PokeballType } from "#enums/pokeball"; import type { PokemonType } from "#enums/pokemon-type"; @@ -706,6 +708,12 @@ export async function catchPokemon( }); }; 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) { const promptRelease = () => { globalScene.ui.showText( diff --git a/src/enums/challenge-type.ts b/src/enums/challenge-type.ts index d9b1fce3e6e..053bcf92011 100644 --- a/src/enums/challenge-type.ts +++ b/src/enums/challenge-type.ts @@ -65,5 +65,45 @@ export enum ChallengeType { /** * 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, } diff --git a/src/enums/challenges.ts b/src/enums/challenges.ts index 7b506a61a2f..951c8707255 100644 --- a/src/enums/challenges.ts +++ b/src/enums/challenges.ts @@ -6,4 +6,7 @@ export enum Challenges { FRESH_START, INVERSE_BATTLE, FLIP_STAT, + LIMITED_CATCH, + NO_SUPPORT, + PERMANENT_FAINT, } diff --git a/src/game-mode.ts b/src/game-mode.ts index c5ab120e218..3aea94b8dda 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -2,8 +2,7 @@ import { FixedBattleConfig } from "#app/battle"; import { CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; -import type { Challenge } from "#data/challenge"; -import { allChallenges, applyChallenges, copyChallenge } from "#data/challenge"; +import { allChallenges, applyChallenges, type Challenge, copyChallenge } from "#data/challenge"; import { getDailyStartingBiome } from "#data/daily-run"; import { allSpecies } from "#data/data-lists"; import type { PokemonSpecies } from "#data/pokemon-species"; @@ -311,6 +310,14 @@ export class GameMode implements GameModeConfig { 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 { switch (this.modeId) { case GameModes.CLASSIC: diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index b359ec756e6..28616bc1b00 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -6,6 +6,7 @@ import Overrides from "#app/overrides"; import { EvolutionItem, pokemonEvolutions } from "#balance/pokemon-evolutions"; import { tmPoolTiers, tmSpecies } from "#balance/tms"; import { getBerryEffectDescription, getBerryName } from "#data/berry"; +import { applyChallenges } from "#data/challenge"; import { allMoves, modifierTypes } from "#data/data-lists"; import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; import { getNatureName, getNatureStatMultiplier } from "#data/nature"; @@ -14,6 +15,7 @@ import { pokemonFormChanges, SpeciesFormChangeCondition } from "#data/pokemon-fo import { getStatusEffectDescriptor } from "#data/status-effect"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; +import { ChallengeType } from "#enums/challenge-type"; import { FormChangeItem } from "#enums/form-change-item"; import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierTier } from "#enums/modifier-tier"; @@ -533,7 +535,7 @@ export class PokemonReviveModifierType extends PokemonHpRestoreModifierType { ); this.selectFilter = (pokemon: PlayerPokemon) => { - if (pokemon.hp) { + if (pokemon.hp || applyChallenges(ChallengeType.PREVENT_REVIVE)) { return PartyUiHandler.NoEffectMessage; } return null; @@ -1262,7 +1264,7 @@ export class FusePokemonModifierType extends PokemonModifierType { iconImage, (_type, args) => new FusePokemonModifier(this, (args[0] as PlayerPokemon).id, (args[1] as PlayerPokemon).id), (pokemon: PlayerPokemon) => { - if (pokemon.isFusion()) { + if (pokemon.isFusion() || !applyChallenges(ChallengeType.POKEMON_FUSION, pokemon)) { return PartyUiHandler.NoEffectMessage; } return null; @@ -2574,11 +2576,14 @@ function getModifierTypeOptionWithRetry( ): ModifierTypeOption { allowLuckUpgrades = allowLuckUpgrades ?? true; let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); + let candidateValidity = applyChallenges(ChallengeType.WAVE_REWARD, candidate); let r = 0; 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) || + !candidateValidity ) { candidate = getNewModifierTypeOption( party, @@ -2588,6 +2593,7 @@ function getModifierTypeOptionWithRetry( 0, allowLuckUpgrades, ); + candidateValidity = applyChallenges(ChallengeType.WAVE_REWARD, 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.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( diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index fcddd23dd20..b6ffc18b047 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -2,6 +2,7 @@ import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { SubstituteTag } from "#data/battler-tags"; +import { applyChallenges } from "#data/challenge"; import { Gender } from "#data/gender"; import { doPokeballBounceAnim, @@ -12,6 +13,7 @@ import { } from "#data/pokeball"; import { getStatusEffectCatchRateMultiplier } from "#data/status-effect"; import { BattlerIndex } from "#enums/battler-index"; +import { ChallengeType } from "#enums/challenge-type"; import type { PokeballType } from "#enums/pokeball"; import { StatusEffect } from "#enums/status-effect"; import { UiMode } from "#enums/ui-mode"; @@ -287,6 +289,11 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; 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) { const promptRelease = () => { globalScene.ui.showText( diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 14674037fbe..3eba211e9af 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -4,12 +4,14 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { speciesStarterCosts } from "#balance/starters"; import type { EncoreTag } from "#data/battler-tags"; import { TrappedTag } from "#data/battler-tags"; +import { applyChallenges } from "#data/challenge"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleType } from "#enums/battle-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; +import { ChallengeType } from "#enums/challenge-type"; import { Command } from "#enums/command"; import { FieldPosition } from "#enums/field-position"; import { MoveId } from "#enums/move-id"; @@ -220,18 +222,31 @@ export class CommandPhase extends FieldPhase { const move = playerPokemon.getMoveset()[cursor]; globalScene.ui.setMode(UiMode.MESSAGE); - // Decides between a Disabled, Not Implemented, or No PP translation message - const errorMessage = playerPokemon.isMoveRestricted(move.moveId, playerPokemon) - ? playerPokemon - .getRestrictingTag(move.moveId, playerPokemon)! - .selectionDeniedText(playerPokemon, move.moveId) - : move.getName().endsWith(" (N)") - ? "battle:moveNotImplemented" - : "battle:moveNoPP"; + // Set the translation key for why the move cannot be selected. The reasons can be: + // - If the move has been restricted in battle (e.g., Disable). + // - If the move has no more PP. + // - If the move is restricted by a challenge. + // - If the move is not implemented + let cannotUseKey: string; + if (playerPokemon.isMoveRestricted(move.moveId, playerPokemon)) { + 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 globalScene.ui.showText( - i18next.t(errorMessage, { moveName: moveName }), + i18next.t(cannotUseKey, { moveName: moveName }), null, () => { globalScene.ui.clearText(); diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index 80d8b315102..f89efaf7c72 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -1,4 +1,6 @@ import { globalScene } from "#app/global-scene"; +import { applyChallenges } from "#data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; import { BattlePhase } from "#phases/battle-phase"; import { fixedInt } from "#utils/common"; @@ -20,13 +22,16 @@ export class PartyHealPhase extends BattlePhase { globalScene.fadeOutBgm(1000, false); } globalScene.ui.fadeOut(1000).then(() => { + const preventRevive = applyChallenges(ChallengeType.PREVENT_REVIVE); for (const pokemon of globalScene.getPlayerParty()) { - pokemon.hp = pokemon.getMaxHp(); - pokemon.resetStatus(true, false, false, true); - for (const move of pokemon.moveset) { - move.ppUsed = 0; + if (!(pokemon.isFainted() && preventRevive)) { + 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/select-biome-phase.ts b/src/phases/select-biome-phase.ts index ab96bf5c45e..a0f3e1cc982 100644 --- a/src/phases/select-biome-phase.ts +++ b/src/phases/select-biome-phase.ts @@ -1,6 +1,8 @@ import { globalScene } from "#app/global-scene"; import { biomeLinks, getBiomeName } from "#balance/biomes"; +import { applyChallenges } from "#data/challenge"; import { BiomeId } from "#enums/biome-id"; +import { ChallengeType } from "#enums/challenge-type"; import { UiMode } from "#enums/ui-mode"; import { MapModifier, MoneyInterestModifier } from "#modifiers/modifier"; import { BattlePhase } from "#phases/battle-phase"; @@ -20,7 +22,9 @@ export class SelectBiomePhase extends BattlePhase { const setNextBiome = (nextBiome: BiomeId) => { if (nextWaveIndex % 10 === 1) { globalScene.applyModifiers(MoneyInterestModifier, true); - globalScene.phaseManager.unshiftNew("PartyHealPhase", false); + if (applyChallenges(ChallengeType.PARTY_HEAL)) { + globalScene.phaseManager.unshiftNew("PartyHealPhase", false); + } } globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome); this.end(); diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 4b1a79d7443..dde1ca66a2a 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -1,8 +1,10 @@ import { timedEventManager } from "#app/global-event-manager"; import { globalScene } from "#app/global-scene"; +import { applyChallenges } from "#data/challenge"; import { modifierTypes } from "#data/data-lists"; import { BattleType } from "#enums/battle-type"; import type { BattlerIndex } from "#enums/battler-index"; +import { ChallengeType } from "#enums/challenge-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import type { CustomModifierSettings } from "#modifiers/modifier-type"; import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils"; @@ -63,7 +65,7 @@ export class VictoryPhase extends PokemonPhase { break; } } - if (globalScene.currentBattle.waveIndex % 10) { + if (globalScene.currentBattle.waveIndex % 10 !== 0 || !applyChallenges(ChallengeType.PARTY_HEAL)) { globalScene.phaseManager.pushNew( "SelectModifierPhase", undefined, diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 50d88738d32..34ef5d0cee7 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -209,10 +209,10 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.updateRerollCostText(); const typeOptions = args[1] as ModifierTypeOption[]; - const removeHealShop = globalScene.gameMode.hasNoShop; + const hasShop = globalScene.gameMode.getShopAvailability(); const baseShopCost = new NumberHolder(globalScene.getWaveMoneyAmount(1)); globalScene.applyModifier(HealShopCostModifier, true, baseShopCost); - const shopTypeOptions = !removeHealShop + const shopTypeOptions = hasShop ? getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value) : []; const optionsYOffset = @@ -370,7 +370,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { if (globalScene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) { this.setRowCursor(0); this.setCursor(2); - } else if (globalScene.shopCursorTarget === ShopCursorTarget.SHOP && globalScene.gameMode.hasNoShop) { + } else if (globalScene.shopCursorTarget === ShopCursorTarget.SHOP && !hasShop) { this.setRowCursor(ShopCursorTarget.REWARDS); this.setCursor(0); } else {