diff --git a/src/data/challenge.ts b/src/data/challenge.ts index a64a90e5d14..5bff8b8bd3c 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -1,21 +1,22 @@ -import * as Utils from "#app/utils"; -import i18next from "i18next"; -import { defaultStarterSpecies, DexAttrProps, GameData } from "#app/system/game-data"; -import PokemonSpecies, { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; -import { speciesStarterCosts } from "#app/data/balance/starters"; -import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { BattleType, FixedBattleConfig } from "#app/battle"; +import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import { speciesStarterCosts } from "#app/data/balance/starters"; +import { Nature } from "#app/data/nature"; +import { pokemonFormChanges } from "#app/data/pokemon-forms"; +import PokemonSpecies, { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; +import { Type } from "#app/data/type"; +import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Trainer, { TrainerVariant } from "#app/field/trainer"; import { GameMode } from "#app/game-mode"; -import { Type } from "#app/data/type"; +import { ModifierTypeOption } from "#app/modifier/modifier-type"; +import { defaultStarterSpecies, DexAttrProps, GameData } from "#app/system/game-data"; +import { BooleanHolder, NumberHolder, randSeedItem } from "#app/utils"; import { Challenges } from "#enums/challenges"; +import { TypeColor, TypeShadow } from "#enums/color"; +import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; -import { Nature } from "#app/data/nature"; -import { Moves } from "#enums/moves"; -import { TypeColor, TypeShadow } from "#enums/color"; -import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; -import { pokemonFormChanges } from "#app/data/pokemon-forms"; +import i18next from "i18next"; /** A constant for the default max cost of the starting party before a run */ const DEFAULT_PARTY_MAX_COST = 10; @@ -27,32 +28,32 @@ const DEFAULT_PARTY_MAX_COST = 10; export enum ChallengeType { /** * Challenges which modify what starters you can choose - * @see {@link Challenge.applyStarterChoice} + * @see {@linkcode Challenge.applyStarterChoice} */ STARTER_CHOICE, /** * Challenges which modify how many starter points you have - * @see {@link Challenge.applyStarterPoints} + * @see {@linkcode Challenge.applyStarterPoints} */ STARTER_POINTS, /** * Challenges which modify how many starter points you have - * @see {@link Challenge.applyStarterPointCost} + * @see {@linkcode Challenge.applyStarterPointCost} */ STARTER_COST, /** * Challenges which modify your starters in some way - * @see {@link Challenge.applyStarterModify} + * @see {@linkcode Challenge.applyStarterModify} */ STARTER_MODIFY, /** * Challenges which limit which pokemon you can have in battle. - * @see {@link Challenge.applyPokemonInBattle} + * @see {@linkcode Challenge.applyPokemonInBattle} */ POKEMON_IN_BATTLE, /** * Adds or modifies the fixed battles in a run - * @see {@link Challenge.applyFixedBattle} + * @see {@linkcode Challenge.applyFixedBattle} */ FIXED_BATTLES, /** @@ -84,6 +85,36 @@ export enum ChallengeType { * Modifies what weight AI pokemon have when generating movesets. UNIMPLEMENTED. */ MOVE_WEIGHT, + /** + * Checks if the heal phase should be run + * @see {@linkcode Challenge.applyNoHealPhase} + */ + NO_HEAL_PHASE, + /** + * Checks if the shop item is blacklisted + * @see {@linkcode Challenge.applyShopItemBlacklist} + */ + SHOP_ITEM_BLACKLIST, + /** + * Checks if the random item is blacklisted + * @see {@linkcode Challenge.applyRandomItemBlacklist} + */ + RANDOM_ITEM_BLACKLIST, + /** + * Checks if the caught pokemon can be added to the team + * @see {@linkcode Challenge.applyAddPokemonToParty} + */ + ADD_POKEMON_TO_PARTY, + /** + * Checks if the move is blacklisted + * @see {@linkcode Challenge.applyMoveBlacklist} + */ + MOVE_BLACKLIST, + /** + * Checks if pokemon are allowed to be revived from fainting + * @see {@linkcode Challenge.applyRevivePrevention} + */ + PREVENT_REVIVE, } /** @@ -103,19 +134,23 @@ export enum MoveSourceType { * A challenge object. Exists only to serve as a base class. */ export abstract class Challenge { - public id: Challenges; // The id of the challenge - - public value: integer; // The "strength" of the challenge, all challenges have a numerical value. - public maxValue: integer; // The maximum strength of the challenge. - public severity: integer; // The current severity of the challenge. Some challenges have multiple severities in addition to strength. - public maxSeverity: integer; // The maximum severity of the challenge. - + /** The id of the challenge */ + public id: Challenges; + /** The "strength" of the challenge. All challenges have a numerical value. */ + public value: number; + /** The maximum strength of the challenge. */ + public maxValue: number; + /** The current severity of the challenge. Some challenges have multiple severities in addition to strength. */ + public severity: number; + /** The maximum severity of the challenge. */ + public maxSeverity: number; + /** Unlock conditions of the challenge. */ public conditions: ChallengeCondition[]; /** - * @param id {@link Challenges} The enum value for the challenge + * @param id {@linkcode Challenges} The enum value for the challenge */ - constructor(id: Challenges, maxValue: integer = Number.MAX_SAFE_INTEGER) { + constructor(id: Challenges, maxValue: number = Number.MAX_SAFE_INTEGER) { this.id = id; this.value = 0; @@ -135,7 +170,7 @@ export abstract class Challenge { /** * Gets the localisation key for the challenge - * @returns {@link string} The i18n key for this challenge + * @returns The i18n key for this challenge */ geti18nKey(): string { return Challenges[this.id].split("_").map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join(""); @@ -144,7 +179,7 @@ export abstract class Challenge { /** * Used for unlockable challenges to check if they're unlocked. * @param data {@link GameData} The save data. - * @returns {@link boolean} Whether this challenge is unlocked. + * @returns `true` if this challenge is unlocked. */ isUnlocked(data: GameData): boolean { return this.conditions.every(f => f(data)); @@ -153,7 +188,7 @@ export abstract class Challenge { /** * Adds an unlock condition to this challenge. * @param condition {@link ChallengeCondition} The condition to add. - * @returns {@link Challenge} This challenge + * @returns This {@linkcode Challenge} */ condition(condition: ChallengeCondition): Challenge { this.conditions.push(condition); @@ -162,7 +197,7 @@ export abstract class Challenge { } /** - * @returns {@link string} The localised name of this challenge. + * @returns The localised name of this challenge. */ getName(): string { return i18next.t(`challenges:${this.geti18nKey()}.name`); @@ -170,8 +205,8 @@ export abstract class Challenge { /** * Returns the textual representation of a challenge's current value. - * @param overrideValue {@link integer} The value to check for. If undefined, gets the current value. - * @returns {@link string} The localised name for the current value. + * @param overrideValue The value to check for. If `undefined`, gets the current value. + * @returns The localised name for the current value. */ getValue(overrideValue?: number): string { const value = overrideValue ?? this.value; @@ -180,8 +215,8 @@ export abstract class Challenge { /** * Returns the description of a challenge's current value. - * @param overrideValue {@link integer} The value to check for. If undefined, gets the current value. - * @returns {@link string} The localised description for the current value. + * @param overrideValue The value to check for. If `undefined`, gets the current value. + * @returns The localised description for the current value. */ getDescription(overrideValue?: number): string { const value = overrideValue ?? this.value; @@ -190,7 +225,7 @@ export abstract class Challenge { /** * Increase the value of the challenge - * @returns {@link boolean} Returns true if the value changed + * @returns `true` if the value changed */ increaseValue(): boolean { if (this.value < this.maxValue) { @@ -202,7 +237,7 @@ export abstract class Challenge { /** * Decrease the value of the challenge - * @returns {@link boolean} Returns true if the value changed + * @returns `true` if the value changed */ decreaseValue(): boolean { if (this.value > 0) { @@ -213,7 +248,7 @@ export abstract class Challenge { } /** - * Whether to allow choosing this challenge's severity. + * @returns Whether to allow choosing this challenge's severity. */ hasSeverity(): boolean { return this.value !== 0 && this.maxSeverity > 0; @@ -221,7 +256,7 @@ export abstract class Challenge { /** * Decrease the severity of the challenge - * @returns {@link boolean} Returns true if the value changed + * @returns `true` if the value changed */ decreaseSeverity(): boolean { if (this.severity > 0) { @@ -233,7 +268,7 @@ export abstract class Challenge { /** * Increase the severity of the challenge - * @returns {@link boolean} Returns true if the value changed + * @returns `true` if the value changed */ increaseSeverity(): boolean { if (this.severity < this.maxSeverity) { @@ -245,160 +280,220 @@ export abstract class Challenge { /** * Gets the "difficulty" value of this challenge. - * @returns {@link integer} The difficulty value. + * @returns The difficulty value. */ - getDifficulty(): integer { + getDifficulty(): number { return this.value; } /** * Gets the minimum difficulty added by this challenge. - * @returns {@link integer} The difficulty value. + * @returns The minimum difficulty value. */ - getMinDifficulty(): integer { + getMinDifficulty(): number { return 0; } /** * Clones a challenge, either from another challenge or json. Chainable. * @param source The source challenge or json. - * @returns This challenge. + * @returns This {@linkcode Challenge}. */ static loadChallenge(source: Challenge | any): Challenge { throw new Error("Method not implemented! Use derived class"); } /** - * An apply function for STARTER_CHOICE challenges. Derived classes should alter this. - * @param pokemon {@link PokemonSpecies} The pokemon to check the validity of. - * @param valid {@link Utils.BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. - * @param dexAttr {@link DexAttrProps} The dex attributes of the pokemon. - * @param soft {@link boolean} If true, allow it if it could become a valid pokemon. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.STARTER_CHOICE} challenges. Derived classes should alter this. + * @param pokemon {@linkcode PokemonSpecies} The pokemon to check the validity of. + * @param valid A {@linkcode BooleanHolder}, the value gets set to `false` if the pokemon isn't allowed. + * @param dexAttr {@linkcode DexAttrProps} The dex attributes of the pokemon. + * @param soft If `true`, allow it if it could become a valid pokemon. + * @returns `true` if this function did anything. */ - applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder, dexAttr: DexAttrProps, soft: boolean = false): boolean { + applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder, dexAttr: DexAttrProps, soft: boolean = false): boolean { return false; } /** - * An apply function for STARTER_POINTS challenges. Derived classes should alter this. - * @param points {@link Utils.NumberHolder} The amount of points you have available. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.STARTER_POINTS} challenges. Derived classes should alter this. + * @param points {@linkcode NumberHolder} The amount of points you have available. + * @returns `true` if this function did anything. */ - applyStarterPoints(points: Utils.NumberHolder): boolean { + applyStarterPoints(points: NumberHolder): boolean { return false; } /** - * An apply function for STARTER_COST challenges. Derived classes should alter this. - * @param species {@link Species} The pokemon to change the cost of. - * @param cost {@link Utils.NumberHolder} The cost of the starter. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.STARTER_COST} challenges. Derived classes should alter this. + * @param species {@linkcode Species} The pokemon to change the cost of. + * @param cost {@link NumberHolder} The cost of the starter. + * @returns `true` if this function did anything. */ - applyStarterCost(species: Species, cost: Utils.NumberHolder): boolean { + applyStarterCost(species: Species, cost: NumberHolder): boolean { return false; } /** - * An apply function for STARTER_MODIFY challenges. Derived classes should alter this. - * @param pokemon {@link Pokemon} The starter pokemon to modify. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.STARTER_MODIFY} challenges. Derived classes should alter this. + * @param pokemon {@linkcode Pokemon} The starter pokemon to modify. + * @returns `true` if this function did anything. */ applyStarterModify(pokemon: Pokemon): boolean { return false; } /** - * An apply function for POKEMON_IN_BATTLE challenges. Derived classes should alter this. - * @param pokemon {@link Pokemon} The pokemon to check the validity of. - * @param valid {@link Utils.BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.POKEMON_IN_BATTLE} challenges. Derived classes should alter this. + * @param pokemon {@linkcode Pokemon} The pokemon to check the validity of. + * @param valid A {@linkcode BooleanHolder}, the value gets set to `false` if the pokemon isn't allowed. + * @returns `true` if this function did anything. */ - applyPokemonInBattle(pokemon: Pokemon, valid: Utils.BooleanHolder): boolean { + applyPokemonInBattle(pokemon: Pokemon, valid: BooleanHolder): boolean { return false; } /** - * An apply function for FIXED_BATTLE challenges. Derived classes should alter this. - * @param waveIndex {@link Number} The current wave index. - * @param battleConfig {@link FixedBattleConfig} The battle config to modify. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.FIXED_BATTLE} challenges. Derived classes should alter this. + * @param waveIndex The current wave index. + * @param battleConfig {@linkcode FixedBattleConfig} The battle config to modify. + * @returns `true` if this function did anything. */ - applyFixedBattle(waveIndex: Number, battleConfig: FixedBattleConfig): boolean { + applyFixedBattle(waveIndex: number, battleConfig: FixedBattleConfig): boolean { return false; } /** - * An apply function for TYPE_EFFECTIVENESS challenges. Derived classes should alter this. - * @param effectiveness {@linkcode Utils.NumberHolder} The current effectiveness of the move. - * @returns Whether this function did anything. + * An apply function for {@linkcode ChallengeType.TYPE_EFFECTIVENESS} challenges. Derived classes should alter this. + * @param effectiveness {@linkcode NumberHolder} The current effectiveness of the move. + * @returns `true` if this function did anything. */ - applyTypeEffectiveness(effectiveness: Utils.NumberHolder): boolean { + applyTypeEffectiveness(effectiveness: NumberHolder): boolean { return false; } /** - * An apply function for AI_LEVEL challenges. Derived classes should alter this. - * @param level {@link Utils.IntegerHolder} The generated level. - * @param levelCap {@link Number} The current level cap. - * @param isTrainer {@link Boolean} Whether this is a trainer pokemon. - * @param isBoss {@link Boolean} Whether this is a non-trainer boss pokemon. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.AI_LEVEL} challenges. Derived classes should alter this. + * @param level {@linkcode NumberHolder} The generated level. + * @param levelCap The current level cap. + * @param isTrainer Whether this is a trainer pokemon. + * @param isBoss Whether this is a non-trainer boss pokemon. + * @returns `true` if this function did anything. */ - applyLevelChange(level: Utils.IntegerHolder, levelCap: number, isTrainer: boolean, isBoss: boolean): boolean { + applyLevelChange(level: NumberHolder, levelCap: number, isTrainer: boolean, isBoss: boolean): boolean { return false; } /** - * An apply function for AI_MOVE_SLOTS challenges. Derived classes should alter this. - * @param pokemon {@link Pokemon} The pokemon that is being considered. - * @param moveSlots {@link Utils.IntegerHolder} The amount of move slots. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.AI_MOVE_SLOTS} challenges. Derived classes should alter this. + * @param pokemon {@linkcode Pokemon} The pokemon that is being considered. + * @param moveSlots {@linkcode NumberHolder} The amount of move slots. + * @returns `true` if this function did anything. */ - applyMoveSlot(pokemon: Pokemon, moveSlots: Utils.IntegerHolder): boolean { + applyMoveSlot(pokemon: Pokemon, moveSlots: NumberHolder): boolean { return false; } /** - * An apply function for PASSIVE_ACCESS challenges. Derived classes should alter this. - * @param pokemon {@link Pokemon} The pokemon to change. - * @param hasPassive {@link Utils.BooleanHolder} Whether it should have its passive. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.PASSIVE_ACCESS} challenges. Derived classes should alter this. + * @param pokemon {@linkcode Pokemon} The pokemon to change. + * @param hasPassive Whether it should have its passive. + * @returns `true` if this function did anything. */ - applyPassiveAccess(pokemon: Pokemon, hasPassive: Utils.BooleanHolder): boolean { + applyPassiveAccess(pokemon: Pokemon, hasPassive: BooleanHolder): boolean { return false; } /** - * An apply function for GAME_MODE_MODIFY challenges. Derived classes should alter this. - * @param gameMode {@link GameMode} The current game mode. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.GAME_MODE_MODIFY} challenges. Derived classes should alter this. + * @param gameMode {@linkcode GameMode} The current game mode. + * @returns `true` if this function did anything. */ applyGameModeModify(gameMode: GameMode): boolean { return false; } /** - * An apply function for MOVE_ACCESS. Derived classes should alter this. - * @param pokemon {@link Pokemon} What pokemon would learn the move. - * @param moveSource {@link MoveSourceType} What source the pokemon would get the move from. - * @param move {@link Moves} The move in question. - * @param level {@link Utils.IntegerHolder} The level threshold for access. - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.MOVE_ACCESS} challenges. Derived classes should alter this. + * @param pokemon {@linkcode Pokemon} What pokemon would learn the move. + * @param moveSource {@linkcode MoveSourceType} What source the pokemon would get the move from. + * @param move {@linkcode Moves} The move in question. + * @param level {@linkcode NumberHolder} The level threshold for access. + * @returns `true` if this function did anything. */ - applyMoveAccessLevel(pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, level: Utils.IntegerHolder): boolean { + applyMoveAccessLevel(pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, level: NumberHolder): boolean { return false; } /** - * An apply function for MOVE_WEIGHT. Derived classes should alter this. - * @param pokemon {@link Pokemon} What pokemon would learn the move. - * @param moveSource {@link MoveSourceType} What source the pokemon would get the move from. - * @param move {@link Moves} The move in question. - * @param weight {@link Utils.IntegerHolder} The base weight of the move - * @returns {@link boolean} Whether this function did anything. + * An apply function for {@linkcode ChallengeType.MOVE_WEIGHT} challenges. Derived classes should alter this. + * @param pokemon {@linkcode Pokemon} What pokemon would learn the move. + * @param moveSource {@linkcode MoveSourceType} What source the pokemon would get the move from. + * @param move {@linkcode Moves} The move in question. + * @param weight {@linkcode NumberHolder} The base weight of the move + * @returns `true` if this function did anything. */ - applyMoveWeight(pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, level: Utils.IntegerHolder): boolean { + applyMoveWeight(pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, level: NumberHolder): boolean { + return false; + } + + /** + * An apply function for {@linkcode ChallengeType.NO_HEAL_PHASE} challenges. Derived classes should alter this. + * @param applyHealPhase {@link BooleanHolder} Whether it should apply the heal phase. + * @returns `true` if this function did anything. + */ + applyNoHealPhase(applyHealPhase: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for {@linkcode ChallengeType.SHOP_ITEM_BLACKLIST} challenges. Derived classes should alter this. + * @param shopItem {@linkcode ModifierTypeOption} The shop item. + * @param isValid {@linkcode BooleanHolder} Whether this item is valid for this challenge. + * @returns `true` if this function did anything. + */ + applyShopItemBlacklist(shopItem: ModifierTypeOption, isValid: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for {@linkcode ChallengeType.RANDOM_ITEM_BLACKLIST} challenges. Derived classes should alter this. + * @param randomItem {@linkcode ModifierTypeOption} The random item. + * @param isValid {@linkcode BooleanHolder} Whether this item is valid for this challenge. + * @returns `true` if this function did anything. + */ + applyRandomItemBlacklist(randomItem: ModifierTypeOption | null, isValid: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for {@linkcode ChallengeType.ADD_POKEMON_TO_PARTY} challenges. Derived classes should alter this. + * @param pokemon {@linkcode EnemyPokemon} The pokemon caught. + * @param waveIndex Current wave index. + * @param canAddToParty {@linkcode BooleanHolder} Whether this pokemon can be added to the party. + * @returns `true` if this function did anything. + */ + applyAddPokemonToParty(pokemon: EnemyPokemon, waveIndex: number, canAddToParty: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for {@linkcode ChallengeType.MOVE_BLACKLIST} challenges. Derived classes should alter this. + * @param move {@linkcode PokemonMove} The move being attempted. + * @param moveCanBeUsed {@linkcode BooleanHolder} Whether this move can be used. + * @returns `true` if this function did anything. + */ + applyMoveBlacklist(move: PokemonMove, moveCanBeUsed: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for {@linkcode ChallengeType.PREVENT_REVIVE} challenges. Derived classes should alter this. + * @param pokemon The {@linkcode PlayerPokemon} being revived + * @param canBeRevived {@linkcode BooleanHolder} Whether the pokemon can be revived. + * @returns `true` if this function did anything. + */ + applyRevivePrevention(pokemon: PlayerPokemon, canBeRevived: BooleanHolder): boolean { return false; } } @@ -413,7 +508,7 @@ export class SingleGenerationChallenge extends Challenge { super(Challenges.SINGLE_GENERATION, 9); } - applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder, dexAttr: DexAttrProps, soft: boolean = false): boolean { + override applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder, dexAttr: DexAttrProps, soft: boolean = false): boolean { const generations = [ pokemon.generation ]; if (soft) { const speciesToCheck = [ pokemon.speciesId ]; @@ -435,7 +530,7 @@ export class SingleGenerationChallenge extends Challenge { return false; } - applyPokemonInBattle(pokemon: Pokemon, valid: Utils.BooleanHolder): boolean { + override applyPokemonInBattle(pokemon: Pokemon, valid: BooleanHolder): boolean { const baseGeneration = pokemon.species.speciesId === Species.VICTINI ? 5 : getPokemonSpecies(pokemon.species.speciesId).generation; const fusionGeneration = pokemon.isFusion() ? pokemon.fusionSpecies?.speciesId === Species.VICTINI ? 5 : getPokemonSpecies(pokemon.fusionSpecies!.speciesId).generation : 0; // TODO: is the bang on fusionSpecies correct? if (pokemon.isPlayer() && (baseGeneration !== this.value || (pokemon.isFusion() && fusionGeneration !== this.value))) { @@ -445,23 +540,23 @@ export class SingleGenerationChallenge extends Challenge { return false; } - applyFixedBattle(waveIndex: Number, battleConfig: FixedBattleConfig): boolean { + override applyFixedBattle(waveIndex: Number, battleConfig: FixedBattleConfig): boolean { let trainerTypes: TrainerType[] = []; switch (waveIndex) { case 182: - trainerTypes = [ TrainerType.LORELEI, TrainerType.WILL, TrainerType.SIDNEY, TrainerType.AARON, TrainerType.SHAUNTAL, TrainerType.MALVA, Utils.randSeedItem([ TrainerType.HALA, TrainerType.MOLAYNE ]), TrainerType.MARNIE_ELITE, TrainerType.RIKA ]; + trainerTypes = [ TrainerType.LORELEI, TrainerType.WILL, TrainerType.SIDNEY, TrainerType.AARON, TrainerType.SHAUNTAL, TrainerType.MALVA, randSeedItem([ TrainerType.HALA, TrainerType.MOLAYNE ]), TrainerType.MARNIE_ELITE, TrainerType.RIKA ]; break; case 184: trainerTypes = [ TrainerType.BRUNO, TrainerType.KOGA, TrainerType.PHOEBE, TrainerType.BERTHA, TrainerType.MARSHAL, TrainerType.SIEBOLD, TrainerType.OLIVIA, TrainerType.NESSA_ELITE, TrainerType.POPPY ]; break; case 186: - trainerTypes = [ TrainerType.AGATHA, TrainerType.BRUNO, TrainerType.GLACIA, TrainerType.FLINT, TrainerType.GRIMSLEY, TrainerType.WIKSTROM, TrainerType.ACEROLA, Utils.randSeedItem([ TrainerType.BEA_ELITE, TrainerType.ALLISTER_ELITE ]), TrainerType.LARRY_ELITE ]; + trainerTypes = [ TrainerType.AGATHA, TrainerType.BRUNO, TrainerType.GLACIA, TrainerType.FLINT, TrainerType.GRIMSLEY, TrainerType.WIKSTROM, TrainerType.ACEROLA, randSeedItem([ TrainerType.BEA_ELITE, TrainerType.ALLISTER_ELITE ]), TrainerType.LARRY_ELITE ]; break; case 188: trainerTypes = [ TrainerType.LANCE, TrainerType.KAREN, TrainerType.DRAKE, TrainerType.LUCIAN, TrainerType.CAITLIN, TrainerType.DRASNA, TrainerType.KAHILI, TrainerType.RAIHAN_ELITE, TrainerType.HASSEL ]; break; case 190: - trainerTypes = [ TrainerType.BLUE, Utils.randSeedItem([ TrainerType.RED, TrainerType.LANCE_CHAMPION ]), Utils.randSeedItem([ TrainerType.STEVEN, TrainerType.WALLACE ]), TrainerType.CYNTHIA, Utils.randSeedItem([ TrainerType.ALDER, TrainerType.IRIS ]), TrainerType.DIANTHA, TrainerType.HAU, TrainerType.LEON, Utils.randSeedItem([ TrainerType.GEETA, TrainerType.NEMONA ]) ]; + trainerTypes = [ TrainerType.BLUE, randSeedItem([ TrainerType.RED, TrainerType.LANCE_CHAMPION ]), randSeedItem([ TrainerType.STEVEN, TrainerType.WALLACE ]), TrainerType.CYNTHIA, randSeedItem([ TrainerType.ALDER, TrainerType.IRIS ]), TrainerType.DIANTHA, TrainerType.HAU, TrainerType.LEON, randSeedItem([ TrainerType.GEETA, TrainerType.NEMONA ]) ]; break; } if (trainerTypes.length === 0) { @@ -472,17 +567,14 @@ export class SingleGenerationChallenge extends Challenge { } } - /** - * @overrides - */ - getDifficulty(): number { + override getDifficulty(): number { return this.value > 0 ? 1 : 0; } /** * Returns the textual representation of a challenge's current value. - * @param {value} overrideValue The value to check for. If undefined, gets the current value. - * @returns {string} The localised name for the current value. + * @param overrideValue The value to check for. If `undefined`, gets the current value. + * @returns The localised name for the current value. */ getValue(overrideValue?: number): string { const value = overrideValue ?? this.value; @@ -494,19 +586,19 @@ export class SingleGenerationChallenge extends Challenge { /** * Returns the description of a challenge's current value. - * @param {value} overrideValue The value to check for. If undefined, gets the current value. - * @returns {string} The localised description for the current value. + * @param overrideValue The value to check for. If `undefined`, gets the current value. + * @returns The localised description for the current value. */ getDescription(overrideValue?: number): string { const value = overrideValue ?? this.value; if (value === 0) { - return i18next.t("challenges:singleGeneration.desc_default"); + return i18next.t(`challenges:${this.geti18nKey()}.desc_default`); } - return i18next.t("challenges:singleGeneration.desc", { gen: i18next.t(`challenges:singleGeneration.gen_${value}`) }); + return i18next.t(`challenges:${this.geti18nKey()}.desc`, { gen: i18next.t(`challenges:${this.geti18nKey()}.gen_${value}`) }); } - static loadChallenge(source: SingleGenerationChallenge | any): SingleGenerationChallenge { + static override loadChallenge(source: SingleGenerationChallenge | any): SingleGenerationChallenge { const newChallenge = new SingleGenerationChallenge(); newChallenge.value = source.value; newChallenge.severity = source.severity; @@ -537,7 +629,7 @@ export class SingleTypeChallenge extends Challenge { super(Challenges.SINGLE_TYPE, 18); } - override applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder, dexAttr: DexAttrProps, soft: boolean = false): boolean { + override applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder, dexAttr: DexAttrProps, soft: boolean = false): boolean { const speciesForm = getPokemonSpeciesForm(pokemon.speciesId, dexAttr.formIndex); const types = [ speciesForm.type1, speciesForm.type2 ]; if (soft && !SingleTypeChallenge.SPECIES_OVERRIDES.includes(pokemon.speciesId)) { @@ -568,7 +660,7 @@ export class SingleTypeChallenge extends Challenge { return false; } - applyPokemonInBattle(pokemon: Pokemon, valid: Utils.BooleanHolder): boolean { + override applyPokemonInBattle(pokemon: Pokemon, valid: BooleanHolder): boolean { if (pokemon.isPlayer() && !pokemon.isOfType(this.value - 1, false, false, true) && !SingleTypeChallenge.TYPE_OVERRIDES.some(o => o.type === (this.value - 1) && (pokemon.isFusion() && o.fusion ? pokemon.fusionSpecies! : pokemon.species).speciesId === o.species)) { // TODO: is the bang on fusionSpecies correct? valid.value = false; @@ -577,42 +669,35 @@ export class SingleTypeChallenge extends Challenge { return false; } - /** - * @overrides - */ - getDifficulty(): number { + override getDifficulty(): number { return this.value > 0 ? 1 : 0; } /** * Returns the textual representation of a challenge's current value. - * @param {value} overrideValue The value to check for. If undefined, gets the current value. - * @returns {string} The localised name for the current value. + * @param overrideValue The value to check for. If `undefined`, gets the current value. + * @returns The localised name for the current value. */ - getValue(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - return Type[this.value - 1].toLowerCase(); + override getValue(overrideValue?: number): string { + const value = overrideValue ?? this.value; + return Type[value - 1].toLowerCase(); } /** * Returns the description of a challenge's current value. - * @param {value} overrideValue The value to check for. If undefined, gets the current value. - * @returns {string} The localised description for the current value. + * @param overrideValue The value to check for. If `undefined`, gets the current value. + * @returns The localised description for the current value. */ - getDescription(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - const type = i18next.t(`pokemonInfo:Type.${Type[this.value - 1]}`); - const typeColor = `[color=${TypeColor[Type[this.value - 1]]}][shadow=${TypeShadow[Type[this.value - 1]]}]${type}[/shadow][/color]`; - const defaultDesc = i18next.t("challenges:singleType.desc_default"); - const typeDesc = i18next.t("challenges:singleType.desc", { type: typeColor }); - return this.value === 0 ? defaultDesc : typeDesc; + override getDescription(overrideValue?: number): string { + const value = overrideValue ?? this.value; + const type = i18next.t(`pokemonInfo:Type.${Type[value - 1]}`); + const typeColor = `[color=${TypeColor[Type[value - 1]]}][shadow=${TypeShadow[Type[value - 1]]}]${type}[/shadow][/color]`; + const defaultDesc = i18next.t(`challenges:${this.geti18nKey()}.desc_default`); + const typeDesc = i18next.t(`challenges:${this.geti18nKey()}.desc`, { type: typeColor }); + return value === 0 ? defaultDesc : typeDesc; } - static loadChallenge(source: SingleTypeChallenge | any): SingleTypeChallenge { + static override loadChallenge(source: SingleTypeChallenge | any): SingleTypeChallenge { const newChallenge = new SingleTypeChallenge(); newChallenge.value = source.value; newChallenge.severity = source.severity; @@ -628,7 +713,7 @@ export class FreshStartChallenge extends Challenge { super(Challenges.FRESH_START, 1); } - applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder): boolean { + override applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean { if (!defaultStarterSpecies.includes(pokemon.speciesId)) { valid.value = false; return true; @@ -636,7 +721,7 @@ export class FreshStartChallenge extends Challenge { return false; } - applyStarterCost(species: Species, cost: Utils.NumberHolder): boolean { + override applyStarterCost(species: Species, cost: NumberHolder): boolean { if (defaultStarterSpecies.includes(species)) { cost.value = speciesStarterCosts[species]; return true; @@ -644,7 +729,7 @@ export class FreshStartChallenge extends Challenge { return false; } - applyStarterModify(pokemon: Pokemon): boolean { + override applyStarterModify(pokemon: Pokemon): boolean { pokemon.abilityIndex = 0; // Always base ability, not hidden ability pokemon.passive = false; // Passive isn't unlocked pokemon.nature = Nature.HARDY; // Neutral nature @@ -661,7 +746,7 @@ export class FreshStartChallenge extends Challenge { return 0; } - static loadChallenge(source: FreshStartChallenge | any): FreshStartChallenge { + static override loadChallenge(source: FreshStartChallenge | any): FreshStartChallenge { const newChallenge = new FreshStartChallenge(); newChallenge.value = source.value; newChallenge.severity = source.severity; @@ -677,7 +762,7 @@ export class InverseBattleChallenge extends Challenge { super(Challenges.INVERSE_BATTLE, 1); } - static loadChallenge(source: InverseBattleChallenge | any): InverseBattleChallenge { + static override loadChallenge(source: InverseBattleChallenge | any): InverseBattleChallenge { const newChallenge = new InverseBattleChallenge(); newChallenge.value = source.value; newChallenge.severity = source.severity; @@ -688,7 +773,7 @@ export class InverseBattleChallenge extends Challenge { return 0; } - applyTypeEffectiveness(effectiveness: Utils.NumberHolder): boolean { + override applyTypeEffectiveness(effectiveness: NumberHolder): boolean { if (effectiveness.value < 1) { effectiveness.value = 2; return true; @@ -709,17 +794,12 @@ export class LowerStarterMaxCostChallenge extends Challenge { super(Challenges.LOWER_MAX_STARTER_COST, 9); } - /** - * @override - */ - getValue(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - return (DEFAULT_PARTY_MAX_COST - overrideValue).toString(); + override getValue(overrideValue?: number): string { + const value = overrideValue ?? this.value; + return (DEFAULT_PARTY_MAX_COST - value).toString(); } - applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder): boolean { + override applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean { if (speciesStarterCosts[pokemon.speciesId] > DEFAULT_PARTY_MAX_COST - this.value) { valid.value = false; return true; @@ -727,7 +807,7 @@ export class LowerStarterMaxCostChallenge extends Challenge { return false; } - static loadChallenge(source: LowerStarterMaxCostChallenge | any): LowerStarterMaxCostChallenge { + static override loadChallenge(source: LowerStarterMaxCostChallenge | any): LowerStarterMaxCostChallenge { const newChallenge = new LowerStarterMaxCostChallenge(); newChallenge.value = source.value; newChallenge.severity = source.severity; @@ -743,22 +823,17 @@ export class LowerStarterPointsChallenge extends Challenge { super(Challenges.LOWER_STARTER_POINTS, 9); } - /** - * @override - */ - getValue(overrideValue?: integer): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - return (DEFAULT_PARTY_MAX_COST - overrideValue).toString(); + override getValue(overrideValue?: number): string { + const value = overrideValue ?? this.value; + return (DEFAULT_PARTY_MAX_COST - value).toString(); } - applyStarterPoints(points: Utils.NumberHolder): boolean { + override applyStarterPoints(points: NumberHolder): boolean { points.value -= this.value; return true; } - static loadChallenge(source: LowerStarterPointsChallenge | any): LowerStarterPointsChallenge { + static override loadChallenge(source: LowerStarterPointsChallenge | any): LowerStarterPointsChallenge { const newChallenge = new LowerStarterPointsChallenge(); newChallenge.value = source.value; newChallenge.severity = source.severity; @@ -766,126 +841,259 @@ export class LowerStarterPointsChallenge extends Challenge { } } +/** Challenge that removes the {@linkcode PartyHealPhase} that occurs after every 10 waves */ +export class NoAutomaticHealChallenge extends Challenge { + constructor() { + super(Challenges.NO_AUTO_HEAL, 1); + } + + override applyNoHealPhase(applyHealPhase: BooleanHolder): boolean { + applyHealPhase.value = false; + return true; + } + + static override loadChallenge(source: NoAutomaticHealChallenge | any): NoAutomaticHealChallenge { + const newChallenge = new NoAutomaticHealChallenge(); + 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, 1); + } + + override applyRandomItemBlacklist(randomItem: ModifierTypeOption | null, isValid: BooleanHolder): boolean { + if (randomItem !== null) { + isValid.value = !this.itemBlackList.includes(randomItem.type.localeKey); + } + return true; + } + + override applyShopItemBlacklist(shopItem: ModifierTypeOption, isValid: BooleanHolder): boolean { + isValid.value = !this.itemBlackList.includes(shopItem.type.localeKey); + return true; + } + + override applyMoveBlacklist(move: PokemonMove, moveCanBeUsed: BooleanHolder): boolean { + const moveBlacklist = [ Moves.REVIVAL_BLESSING ]; + moveCanBeUsed.value = !moveBlacklist.includes(move.moveId); + return true; + } + + override applyRevivePrevention(_pokemon: PlayerPokemon, canBeRevived: BooleanHolder): boolean { + canBeRevived.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 only allows the first pokemon of every 10 waves to be caught */ +export class LimitedCatchChallenge extends Challenge { + constructor() { + super(Challenges.LIMITED_CATCH, 1); + } + + override applyAddPokemonToParty(pokemon: EnemyPokemon, waveIndex: number, canAddToParty: BooleanHolder): boolean { + if (!(waveIndex % 10 === 1) && !(pokemon.scene.lastMysteryEncounter && (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 gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.STARTER_CHOICE - * @param pokemon {@link PokemonSpecies} The pokemon to check the validity of. - * @param valid {@link Utils.BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. - * @param dexAttr {@link DexAttrProps} The dex attributes of the pokemon. - * @param soft {@link boolean} If true, allow it if it could become a valid pokemon. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.STARTER_CHOICE} + * @param pokemon The {@linkcode PokemonSpecies} to check the validity of. + * @param valid {@link BooleanHolder} `false` if the pokemon isn't allowed. + * @param dexAttr {@linkcode DexAttrProps} The dex attributes of the pokemon. + * @param soft If `true`, allow it if it could become a valid pokemon. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.STARTER_CHOICE, pokemon: PokemonSpecies, valid: Utils.BooleanHolder, dexAttr: DexAttrProps, soft: boolean): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.STARTER_CHOICE, pokemon: PokemonSpecies, valid: BooleanHolder, dexAttr: DexAttrProps, soft: boolean): boolean; /** * Apply all challenges that modify available total starter points. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.STARTER_POINTS - * @param points {@link Utils.NumberHolder} The amount of points you have available. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.STARTER_POINTS} + * @param points {@link NumberHolder} The amount of points you have available. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.STARTER_POINTS, points: Utils.NumberHolder): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.STARTER_POINTS, points: NumberHolder): boolean; /** * Apply all challenges that modify the cost of a starter. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.STARTER_COST - * @param species {@link Species} The pokemon to change the cost of. - * @param points {@link Utils.NumberHolder} The cost of the pokemon. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.STARTER_COST} + * @param species The pokemon {@linkcode Species} to change the cost of. + * @param points {@linkcode NumberHolder} The cost of the pokemon. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.STARTER_COST, species: Species, cost: Utils.NumberHolder): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.STARTER_COST, species: Species, cost: NumberHolder): boolean; /** * Apply all challenges that modify a starter after selection. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.STARTER_MODIFY - * @param pokemon {@link Pokemon} The starter pokemon to modify. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.STARTER_MODIFY} + * @param pokemon The starter {@linkcode Pokemon} to modify. + * @returns `true` if any challenge was successfully applied. */ export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.STARTER_MODIFY, pokemon: Pokemon): boolean; /** * Apply all challenges that what pokemon you can have in battle. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.POKEMON_IN_BATTLE - * @param pokemon {@link Pokemon} The pokemon to check the validity of. - * @param valid {@link Utils.BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.POKEMON_IN_BATTLE} + * @param pokemon The {@linkcode Pokemon} Tcheck the validity of. + * @param valid {@link BooleanHolder} `false` if the pokemon isn't allowed. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.POKEMON_IN_BATTLE, pokemon: Pokemon, valid: Utils.BooleanHolder): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.POKEMON_IN_BATTLE, pokemon: Pokemon, valid: BooleanHolder): boolean; /** * Apply all challenges that modify what fixed battles there are. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.FIXED_BATTLES - * @param waveIndex {@link Number} The current wave index. - * @param battleConfig {@link FixedBattleConfig} The battle config to modify. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.FIXED_BATTLES} + * @param waveIndex The current wave index. + * @param battleConfig The {@link FixedBattleConfig} to modify. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.FIXED_BATTLES, waveIndex: Number, battleConfig: FixedBattleConfig): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.FIXED_BATTLES, waveIndex: number, battleConfig: FixedBattleConfig): boolean; /** * Apply all challenges that modify type effectiveness. - * @param gameMode {@linkcode GameMode} The current gameMode - * @param challengeType {@linkcode ChallengeType} ChallengeType.TYPE_EFFECTIVENESS - * @param effectiveness {@linkcode Utils.NumberHolder} The current effectiveness of the move. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.TYPE_EFFECTIVENESS} + * @param effectiveness {@linkcode NumberHolder} The current effectiveness of the move. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.TYPE_EFFECTIVENESS, effectiveness: Utils.NumberHolder): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.TYPE_EFFECTIVENESS, effectiveness: NumberHolder): boolean; /** * Apply all challenges that modify what level AI are. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.AI_LEVEL - * @param level {@link Utils.IntegerHolder} The generated level of the pokemon. - * @param levelCap {@link Number} The maximum level cap for the current wave. - * @param isTrainer {@link Boolean} Whether this is a trainer pokemon. - * @param isBoss {@link Boolean} Whether this is a non-trainer boss pokemon. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.AI_LEVEL} + * @param level {@link NumberHolder} The generated level of the pokemon. + * @param levelCap The maximum level cap for the current wave. + * @param isTrainer Whether this is a trainer pokemon. + * @param isBoss Whether this is a non-trainer boss pokemon. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.AI_LEVEL, level: Utils.IntegerHolder, levelCap: number, isTrainer: boolean, isBoss: boolean): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.AI_LEVEL, level: NumberHolder, levelCap: number, isTrainer: boolean, isBoss: boolean): boolean; /** * Apply all challenges that modify how many move slots the AI has. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.AI_MOVE_SLOTS - * @param pokemon {@link Pokemon} The pokemon being considered. - * @param moveSlots {@link Utils.IntegerHolder} The amount of move slots. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.AI_MOVE_SLOTS} + * @param pokemon The {@linkcode Pokemon} being considered. + * @param moveSlots {@linkcode NumberHolder} The amount of move slots. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.AI_MOVE_SLOTS, pokemon: Pokemon, moveSlots: Utils.IntegerHolder): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.AI_MOVE_SLOTS, pokemon: Pokemon, moveSlots: NumberHolder): boolean; /** * Apply all challenges that modify whether a pokemon has its passive. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.PASSIVE_ACCESS - * @param pokemon {@link Pokemon} The pokemon to modify. - * @param hasPassive {@link Utils.BooleanHolder} Whether it has its passive. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.PASSIVE_ACCESS} + * @param pokemon The {@linkcode Pokemon} to modify. + * @param hasPassive {@linkcode BooleanHolder} Whether it has its passive. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.PASSIVE_ACCESS, pokemon: Pokemon, hasPassive: Utils.BooleanHolder): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.PASSIVE_ACCESS, pokemon: Pokemon, hasPassive: BooleanHolder): boolean; /** * Apply all challenges that modify the game modes settings. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.GAME_MODE_MODIFY - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.GAME_MODE_MODIFY} + * @returns `true` if any challenge was successfully applied. */ export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.GAME_MODE_MODIFY): boolean; /** * Apply all challenges that modify what level a pokemon can access a move. - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.MOVE_ACCESS - * @param pokemon {@link Pokemon} What pokemon would learn the move. - * @param moveSource {@link MoveSourceType} What source the pokemon would get the move from. - * @param move {@link Moves} The move in question. - * @param level {@link Utils.IntegerHolder} The level threshold for access. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.MOVE_ACCESS} + * @param pokemon What {@linkcode Pokemon} would learn the move. + * @param moveSource {@linkcode MoveSourceType} What source the pokemon would get the move from. + * @param move {@linkcode Moves} The move in question. + * @param level {@linkcode NumberHolder} The level threshold for access. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.MOVE_ACCESS, pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, level: Utils.IntegerHolder): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.MOVE_ACCESS, pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, level: NumberHolder): boolean; /** * Apply all challenges that modify what weight a pokemon gives to move generation - * @param gameMode {@link GameMode} The current gameMode - * @param challengeType {@link ChallengeType} ChallengeType.MOVE_WEIGHT - * @param pokemon {@link Pokemon} What pokemon would learn the move. - * @param moveSource {@link MoveSourceType} What source the pokemon would get the move from. - * @param move {@link Moves} The move in question. - * @param weight {@link Utils.IntegerHolder} The weight of the move. - * @returns True if any challenge was successfully applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.MOVE_WEIGHT} + * @param pokemon What {@linkcode Pokemon} would learn the move. + * @param moveSource {@linkcode MoveSourceType} What source the pokemon would get the move from. + * @param move {@linkcode Moves} The move in question. + * @param weight {@linkcode NumberHolder} The weight of the move. + * @returns `true` if any challenge was successfully applied. */ -export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.MOVE_WEIGHT, pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, weight: Utils.IntegerHolder): boolean; +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.MOVE_WEIGHT, pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, weight: NumberHolder): boolean; +/** + * Apply all challenges that modify if the heal phase should be applied. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.NO_HEAL_PHASE} + * @param applyHealPhase {@linkcode BooleanHolder} Whether it should apply the heal phase. + * @returns `true` if any challenge was successfully applied. + */ +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.NO_HEAL_PHASE, applyHealPhase: BooleanHolder): boolean; +/** + * Apply all challenges that modify if this shop item can be bought. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.SHOP_ITEM_BLACKLIST} + * @param shopItem {@linkcode ModifierTypeOption} The shop item. + * @param isValid {@linkcode BooleanHolder} Whether this item is valid for this challenge. + * @returns `true` if any challenge was successfully applied. + */ +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.SHOP_ITEM_BLACKLIST, shopItem: ModifierTypeOption, isValid: BooleanHolder): boolean; +/** + * Apply all challenges that modify if this random item can be generated. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.RANDOM_ITEM_BLACKLIST} + * @param randomItem {@linkcode ModifierTypeOption} The random item. + * @param isValid {@linkcode BooleanHolder} Whether this item is valid for this challenge. + * @returns `true` if any challenge was successfully applied. + */ +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.RANDOM_ITEM_BLACKLIST, randomItem: ModifierTypeOption | null, isValid: BooleanHolder): boolean; +/** + * Apply all challenges that modify if that pokemon can be added to the party. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.ADD_POKEMON_TO_PARTY} + * @param pokemon {@linkcode EnemyPokemon} The pokemon caught. + * @param waveIndex Current wave index. + * @param canBeAddToParty {@linkcode BooleanHolder} Whether this pokemon can be added to the party. + * @returns `true` if any challenge was successfully applied. + */ +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.ADD_POKEMON_TO_PARTY, pokemon: EnemyPokemon, waveIndex: number, canBeAddToParty: BooleanHolder): boolean; +/** + * Apply all challenges that modify if that move can be used. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.MOVE_BLACKLIST} + * @param move {@linkcode PokemonMove} The move being attempted. + * @param moveCanBeUsed {@linkcode BooleanHolder} Whether this move can be used. + * @returns `true` if any challenge was successfully applied. + */ +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.MOVE_BLACKLIST, move: PokemonMove, moveCanBeUsed: BooleanHolder): boolean; +/** + * Apply all challenges that modify if pokemon can be revived. + * @param gameMode The current {@linkcode GameMode} + * @param challengeType {@linkcode ChallengeType.PREVENT_REVIVE} + * @param pokemon The {@linkcode PlayerPokemon} being revived. + * @param canBeRevived {@linkcode BooleanHolder} Whether the pokemon can be revived. + * @returns `true` if any challenge was successfully applied. + */ +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.PREVENT_REVIVE, pokemon: PlayerPokemon, canBeRevived: BooleanHolder): boolean; export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType, ...args: any[]): boolean { let ret = false; gameMode.challenges.forEach(c => { @@ -930,6 +1138,24 @@ export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType case ChallengeType.MOVE_WEIGHT: ret ||= c.applyMoveWeight(args[0], args[1], args[2], args[3]); 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.ADD_POKEMON_TO_PARTY: + ret ||= c.applyAddPokemonToParty(args[0], args[1], args[2]); + break; + case ChallengeType.MOVE_BLACKLIST: + ret ||= c.applyMoveBlacklist(args[0], args[1]); + break; + case ChallengeType.PREVENT_REVIVE: + ret ||= c.applyRevivePrevention(args[0], args[1]); + break; } } }); @@ -955,6 +1181,12 @@ export function copyChallenge(source: Challenge | any): Challenge { return FreshStartChallenge.loadChallenge(source); case Challenges.INVERSE_BATTLE: return InverseBattleChallenge.loadChallenge(source); + case Challenges.NO_AUTO_HEAL: + return NoAutomaticHealChallenge.loadChallenge(source); + case Challenges.HARDCORE: + return HardcoreChallenge.loadChallenge(source); + case Challenges.LIMITED_CATCH: + return LimitedCatchChallenge.loadChallenge(source); } throw new Error("Unknown challenge copied"); } @@ -967,5 +1199,8 @@ export function initChallenges() { new SingleTypeChallenge(), new FreshStartChallenge(), new InverseBattleChallenge(), + new NoAutomaticHealChallenge(), + new HardcoreChallenge(), + new LimitedCatchChallenge(), ); } diff --git a/src/enums/challenges.ts b/src/enums/challenges.ts index c4dc7460dfe..bb5ca8dd63a 100644 --- a/src/enums/challenges.ts +++ b/src/enums/challenges.ts @@ -5,4 +5,7 @@ export enum Challenges { LOWER_STARTER_POINTS, FRESH_START, INVERSE_BATTLE, + NO_AUTO_HEAL, + HARDCORE, + LIMITED_CATCH, } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 3e475c62590..aa6073df701 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -2,6 +2,7 @@ import BattleScene from "#app/battle-scene"; import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms"; import { getBerryEffectDescription, getBerryName } from "#app/data/berry"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; import { allMoves, AttackMove, selfStatLowerMoves } from "#app/data/move"; import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature"; import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "#app/data/pokeball"; @@ -9,6 +10,7 @@ import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, Species import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect"; import { Type } from "#app/data/type"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { GameMode } from "#app/game-mode"; import { getPokemonNameWithAffix } from "#app/messages"; import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier @@ -19,7 +21,7 @@ import { Unlockables } from "#app/system/unlockables"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import { getModifierTierTextTint } from "#app/ui/text"; -import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; +import { BooleanHolder, formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -144,7 +146,7 @@ export class ModifierType { type ModifierTypeGeneratorFunc = (party: Pokemon[], pregenArgs?: any[]) => ModifierType | null; export class ModifierTypeGenerator extends ModifierType { - private genTypeFunc: ModifierTypeGeneratorFunc; + private genTypeFunc: ModifierTypeGeneratorFunc; constructor(genTypeFunc: ModifierTypeGeneratorFunc) { super(null, null, null); @@ -873,7 +875,7 @@ export class FormChangeItemModifierType extends PokemonModifierType implements G if (pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId) // Get all form changes for this species with an item trigger, including any compound triggers && pokemonFormChanges[pokemon.species.speciesId].filter(fc => fc.trigger.hasTriggerType(SpeciesFormChangeItemTrigger) && (fc.preFormKey === pokemon.getFormKey())) - // Returns true if any form changes match this item + // Returns true if any form changes match this item .map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) .flat().flatMap(fc => fc.item).includes(this.formChangeItem) ) { @@ -1272,7 +1274,7 @@ type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: integer) */ function skipInClassicAfterWave(wave: integer, defaultWeight: integer): WeightedModifierTypeWeightFunc { return (party: Pokemon[]) => { - const gameMode = party[0].scene.gameMode; + const gameMode = party[0].scene.gameMode; const currentWave = party[0].scene.currentBattle.waveIndex; return gameMode.isClassic && currentWave >= wave ? 0 : defaultWeight; }; @@ -1284,7 +1286,7 @@ function skipInClassicAfterWave(wave: integer, defaultWeight: integer): Weighted * @param defaultWeight ModifierType default weight * @returns A WeightedModifierTypeWeightFunc */ -function skipInLastClassicWaveOrDefault(defaultWeight: integer) : WeightedModifierTypeWeightFunc { +function skipInLastClassicWaveOrDefault(defaultWeight: number): WeightedModifierTypeWeightFunc { return skipInClassicAfterWave(199, defaultWeight); } @@ -1302,7 +1304,7 @@ function lureWeightFunc(maxBattles: number, weight: number): WeightedModifierTyp }; } -class WeightedModifierType { +export class WeightedModifierType { public modifierType: ModifierType; public weight: integer | WeightedModifierTypeWeightFunc; public maxWeight: integer; @@ -1598,7 +1600,7 @@ export const modifierTypes = { MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET", "golden_net", (type, _args) => new BoostBugSpawnModifier(type)), }; -interface ModifierPool { +export interface ModifierPool { [tier: string]: WeightedModifierType[] } @@ -1910,10 +1912,10 @@ const enemyBuffModifierPool: ModifierPool = { ].map(m => { m.setTier(ModifierTier.ULTRA); return m; }), - [ModifierTier.ROGUE]: [ ].map((m: WeightedModifierType) => { + [ModifierTier.ROGUE]: [].map((m: WeightedModifierType) => { m.setTier(ModifierTier.ROGUE); return m; }), - [ModifierTier.MASTER]: [ ].map((m: WeightedModifierType) => { + [ModifierTier.MASTER]: [].map((m: WeightedModifierType) => { m.setTier(ModifierTier.MASTER); return m; }) }; @@ -2018,7 +2020,7 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod let tierMaxWeight = 0; let i = 0; pool[t].reduce((total: integer, modifierType: WeightedModifierType) => { - const weightedModifierType = modifierType as WeightedModifierType; + const weightedModifierType = modifierType; const existingModifiers = party[0].scene.findModifiers(m => m.type.id === weightedModifierType.modifierType.id, poolType === ModifierPoolType.PLAYER); const itemModifierType = weightedModifierType.modifierType instanceof ModifierTypeGenerator ? weightedModifierType.modifierType.generateType(party) @@ -2028,8 +2030,8 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod || itemModifierType instanceof FormChangeItemModifierType || existingModifiers.find(m => m.stackCount < m.getMaxStackCount(party[0].scene, true)) ? weightedModifierType.weight instanceof Function - ? (weightedModifierType.weight as Function)(party, rerollCount) - : weightedModifierType.weight as integer + ? weightedModifierType.weight(party, rerollCount) + : weightedModifierType.weight : 0; if (weightedModifierType.maxWeight) { const modifierId = weightedModifierType.modifierType.id; @@ -2174,12 +2176,23 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo * @param tier If specified will generate item of tier * @param allowLuckUpgrades `true` to allow items to upgrade tiers (the little animation that plays and is affected by luck) */ -function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier, allowLuckUpgrades?: boolean): ModifierTypeOption { +function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: number, party: PlayerPokemon[], tier?: ModifierTier, allowLuckUpgrades?: boolean): ModifierTypeOption { allowLuckUpgrades = allowLuckUpgrades ?? true; let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); let r = 0; - while (existingOptions.length && ++r < retryCount && existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length) { + let isValidForChallenge = new BooleanHolder(true); + applyChallenges(party[0].scene.gameMode, ChallengeType.RANDOM_ITEM_BLACKLIST, candidate, isValidForChallenge); + while ( + ( + existingOptions.length + && ++r < retryCount + && existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length + ) + || !isValidForChallenge.value + ) { candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate?.type.tier ?? tier, candidate?.upgradeCount, 0, allowLuckUpgrades); + isValidForChallenge = new BooleanHolder(true); + applyChallenges(party[0].scene.gameMode, ChallengeType.RANDOM_ITEM_BLACKLIST, candidate, isValidForChallenge); } return candidate!; } @@ -2209,7 +2222,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[], } } -export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, baseCost: integer): ModifierTypeOption[] { +export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseCost: number, gameMode: GameMode): ModifierTypeOption[] { if (!(waveIndex % 10)) { return []; } @@ -2244,7 +2257,11 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, base new ModifierTypeOption(modifierTypes.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(item => { + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(gameMode, ChallengeType.SHOP_ITEM_BLACKLIST, item, isValidForChallenge); + return isValidForChallenge.value; + }); } export function getEnemyBuffModifierForWave(tier: ModifierTier, enemyModifiers: PersistentModifier[], scene: BattleScene): EnemyPersistentModifier { @@ -2319,8 +2336,8 @@ export function getDailyRunStarterModifiers(party: PlayerPokemon[]): PokemonHeld * @param retryCount Max allowed tries before the next tier down is checked for a valid ModifierType * @param allowLuckUpgrades Default true. If false, will not allow ModifierType to randomly upgrade to next tier */ -function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0, allowLuckUpgrades: boolean = true): ModifierTypeOption | null { - const player = !poolType; +function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: number, retryCount: number = 0, allowLuckUpgrades: boolean = true): ModifierTypeOption | null { + const player = poolType === ModifierPoolType.PLAYER; const pool = getModifierPoolForType(poolType); let thresholds: object; switch (poolType) { @@ -2370,7 +2387,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, } tier += upgradeCount; - while (tier && (!modifierPool.hasOwnProperty(tier) || !modifierPool[tier].length)) { + while (tier && (!pool.hasOwnProperty(tier) || !pool[tier].length)) { tier--; if (upgradeCount) { upgradeCount--; @@ -2381,7 +2398,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, if (tier < ModifierTier.MASTER && allowLuckUpgrades) { const partyShinyCount = party.filter(p => p.isShiny() && !p.isFainted()).length; const upgradeOdds = Math.floor(32 / ((partyShinyCount + 2) / 2)); - while (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[tier + upgradeCount + 1].length) { + while (pool.hasOwnProperty(tier + upgradeCount + 1) && pool[tier + upgradeCount + 1].length) { if (!randSeedInt(upgradeOdds)) { upgradeCount++; } else { diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 3e46fc792f0..6167ab6f89a 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -1,21 +1,23 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { getPokeballCatchMultiplier, getPokeballAtlasKey, getPokeballTintColor, doPokeballBounceAnim } from "#app/data/pokeball"; +import BattleScene from "#app/battle-scene"; +import { SubstituteTag } from "#app/data/battler-tags"; +import { ChallengeType, applyChallenges } from "#app/data/challenge"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { PokeballType } from "#app/enums/pokeball"; import { StatusEffect } from "#app/enums/status-effect"; -import { addPokeballOpenParticles, addPokeballCaptureStars } from "#app/field/anims"; +import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import { EnemyPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; import { achvs } from "#app/system/achv"; -import { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; +import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { Mode } from "#app/ui/ui"; +import { BooleanHolder } from "#app/utils"; import i18next from "i18next"; -import { PokemonPhase } from "./pokemon-phase"; -import { VictoryPhase } from "./victory-phase"; -import { SubstituteTag } from "#app/data/battler-tags"; export class AttemptCapturePhase extends PokemonPhase { private pokeballType: PokeballType; @@ -249,6 +251,13 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => { + const challengeCanAddToParty = new BooleanHolder(true); + applyChallenges(this.scene.gameMode, ChallengeType.ADD_POKEMON_TO_PARTY, pokemon, this.scene.currentBattle.waveIndex, challengeCanAddToParty); + if (!challengeCanAddToParty.value) { + removePokemon(); + end(); + return; + } if (this.scene.getParty().length === 6) { const promptRelease = () => { this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 6d4d46c51c9..7bb9c0fdca7 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -1,22 +1,23 @@ +import { BattleType, TurnCommand } from "#app/battle"; import BattleScene from "#app/battle-scene"; -import { TurnCommand, BattleType } from "#app/battle"; -import { TrappedTag, EncoreTag } from "#app/data/battler-tags"; -import { MoveTargetSet, getMoveTargets } from "#app/data/move"; import { speciesStarterCosts } from "#app/data/balance/starters"; -import { Abilities } from "#app/enums/abilities"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { Biome } from "#app/enums/biome"; -import { Moves } from "#app/enums/moves"; +import { EncoreTag, TrappedTag } from "#app/data/battler-tags"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { getMoveTargets, MoveTargetSet } from "#app/data/move"; import { PokeballType } from "#app/enums/pokeball"; import { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; +import { FieldPhase } from "#app/phases/field-phase"; +import { SelectTargetPhase } from "#app/phases/select-target-phase"; import { Command } from "#app/ui/command-ui-handler"; import { Mode } from "#app/ui/ui"; -import i18next from "i18next"; -import { FieldPhase } from "./field-phase"; -import { SelectTargetPhase } from "./select-target-phase"; +import { BooleanHolder, isNullOrUndefined } from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Biome } from "#enums/biome"; +import { Moves } from "#enums/moves"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { isNullOrUndefined } from "#app/utils"; +import i18next from "i18next"; export class CommandPhase extends FieldPhase { protected fieldIndex: integer; @@ -94,6 +95,18 @@ export class CommandPhase extends FieldPhase { switch (command) { case Command.FIGHT: + // Check if move can be used in challenge + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(this.scene.gameMode, ChallengeType.MOVE_BLACKLIST, playerPokemon.getMoveset()[cursor]!, isValidForChallenge); + if (!isValidForChallenge.value) { + const moveName = playerPokemon.getMoveset()[cursor]?.getName(); + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.ui.showText(i18next.t("challenges:illegalMove", { moveName: moveName }), null, () => { + this.scene.ui.clearText(); + this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex); + }, null, true); + break; + } let useStruggle = false; if (cursor === -1 || playerPokemon.trySelectMove(cursor, args[0] as boolean) || diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index e6ee11202df..b977e10afa1 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -1,6 +1,7 @@ import BattleScene from "#app/battle-scene"; -import * as Utils from "#app/utils"; -import { BattlePhase } from "./battle-phase"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { BattlePhase } from "#app/phases/battle-phase"; +import { BooleanHolder, fixedInt } from "#app/utils"; export class PartyHealPhase extends BattlePhase { private resumeBgm: boolean; @@ -14,21 +15,34 @@ export class PartyHealPhase extends BattlePhase { start() { super.start(); + const isHealPhaseActive = new BooleanHolder(true); + applyChallenges(this.scene.gameMode, ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); + if (!isHealPhaseActive.value) { + return this.end(); + } + const bgmPlaying = this.scene.isBgmPlaying(); if (bgmPlaying) { this.scene.fadeOutBgm(1000, false); } + + const canBeRevived = new BooleanHolder(true); this.scene.ui.fadeOut(1000).then(() => { for (const pokemon of this.scene.getParty()) { - pokemon.hp = pokemon.getMaxHp(); - pokemon.resetStatus(); - for (const move of pokemon.moveset) { - move!.ppUsed = 0; // TODO: is this bang correct? + applyChallenges(this.scene.gameMode, ChallengeType.PREVENT_REVIVE, pokemon, canBeRevived); + if (canBeRevived.value || !pokemon.isFainted()) { + pokemon.hp = pokemon.getMaxHp(); + pokemon.resetStatus(); + for (const move of pokemon.moveset) { + if (move) { + move.ppUsed = 0; + } + } + pokemon.updateInfo(true); } - pokemon.updateInfo(true); } const healSong = this.scene.playSoundWithoutBgm("heal"); - this.scene.time.delayedCall(Utils.fixedInt(healSong.totalDuration * 1000), () => { + this.scene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => { healSong.destroy(); if (this.resumeBgm && bgmPlaying) { this.scene.playBgm(); diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index e5a60692bb4..11f410a0393 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -1,16 +1,35 @@ import BattleScene from "#app/battle-scene"; +import { + ExtraModifierModifier, + HealShopCostModifier, + Modifier, + PokemonHeldItemModifier, + TempExtraModifierModifier +} from "#app/modifier/modifier"; import { ModifierTier } from "#app/modifier/modifier-tier"; -import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type"; -import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; -import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; -import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; -import { Mode } from "#app/ui/ui"; -import i18next from "i18next"; -import * as Utils from "#app/utils"; -import { BattlePhase } from "./battle-phase"; +import { + CustomModifierSettings, + FusePokemonModifierType, + getPlayerModifierTypeOptions, + getPlayerShopModifierTypeOptionsForWave, + ModifierPoolType, + ModifierType, + ModifierTypeOption, + PokemonModifierType, + PokemonMoveModifierType, + PokemonPpRestoreModifierType, + PokemonPpUpModifierType, + regenerateModifierPoolThresholds, + RememberMoveModifierType, + TmModifierType +} from "#app/modifier/modifier-type"; import Overrides from "#app/overrides"; -import { CustomModifierSettings } from "#app/modifier/modifier-type"; +import { BattlePhase } from "#app/phases/battle-phase"; +import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; +import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; +import { Mode } from "#app/ui/ui"; import { isNullOrUndefined, NumberHolder } from "#app/utils"; +import i18next from "i18next"; export class SelectModifierPhase extends BattlePhase { private rerollCount: integer; @@ -42,7 +61,7 @@ export class SelectModifierPhase extends BattlePhase { if (!this.isCopy) { regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); } - const modifierCount = new Utils.IntegerHolder(3); + const modifierCount = new NumberHolder(3); if (this.isPlayer()) { this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount); @@ -140,7 +159,7 @@ export class SelectModifierPhase extends BattlePhase { } break; default: - const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1)); + const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1), this.scene.gameMode); const shopOption = shopOptions[rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT]; if (shopOption.type) { modifierType = shopOption.type; diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 1faa31655df..7c40dc2a673 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -1,16 +1,18 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex, BattleType, ClassicFixedBossWaves } from "#app/battle"; -import { CustomModifierSettings, modifierTypes } from "#app/modifier/modifier-type"; -import { BattleEndPhase } from "./battle-end-phase"; -import { NewBattlePhase } from "./new-battle-phase"; -import { PokemonPhase } from "./pokemon-phase"; -import { AddEnemyBuffModifierPhase } from "./add-enemy-buff-modifier-phase"; -import { EggLapsePhase } from "./egg-lapse-phase"; -import { GameOverPhase } from "./game-over-phase"; -import { ModifierRewardPhase } from "./modifier-reward-phase"; -import { SelectModifierPhase } from "./select-modifier-phase"; -import { TrainerVictoryPhase } from "./trainer-victory-phase"; +import BattleScene from "#app/battle-scene"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { CustomModifierSettings, modifierTypes } from "#app/modifier/modifier-type"; +import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase"; +import { BattleEndPhase } from "#app/phases/battle-end-phase"; +import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { TrainerVictoryPhase } from "#app/phases/trainer-victory-phase"; +import { BooleanHolder } from "#app/utils"; export class VictoryPhase extends PokemonPhase { /** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */ @@ -40,6 +42,9 @@ export class VictoryPhase extends PokemonPhase { return this.end(); } + const isHealPhaseActive = new BooleanHolder(true); + applyChallenges(this.scene.gameMode, ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); + if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) { this.scene.pushPhase(new BattleEndPhase(this.scene)); if (this.scene.currentBattle.battleType === BattleType.TRAINER) { @@ -51,7 +56,7 @@ export class VictoryPhase extends PokemonPhase { // Should get Lock Capsule on 165 before shop phase so it can be used in the rewards shop this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.LOCK_CAPSULE)); } - if (this.scene.currentBattle.waveIndex % 10) { + if (this.scene.currentBattle.waveIndex % 10 || (this.scene.currentBattle.waveIndex === 0 && !isHealPhaseActive.value)) { this.scene.pushPhase(new SelectModifierPhase(this.scene, undefined, undefined, this.getFixedBattleCustomModifiers())); } else if (this.scene.gameMode.isDaily) { this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM)); diff --git a/src/test/challenge/hardcore.test.ts b/src/test/challenge/hardcore.test.ts new file mode 100644 index 00000000000..0b9615f886d --- /dev/null +++ b/src/test/challenge/hardcore.test.ts @@ -0,0 +1,62 @@ +import { Abilities } from "#enums/abilities"; +import { Challenges } from "#enums/challenges"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Challenge - Hardcore", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.challengeMode.addChallenge(Challenges.HARDCORE); + + game.override + .battleType("single") + .ability(Abilities.BALL_FETCH) + .moveset(Moves.THUNDERBOLT) + .startingLevel(2000) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it.todo("prevents revival items from showing up in the shop", async () => { + game.override.startingWave(191); + await game.challengeMode.startBattle(); + + game.move.select(Moves.THUNDERBOLT); + await game.phaseInterceptor.to("SelectModifierPhase"); + + expect(1); + }); + + it.todo("prevents revival items from showing up in rewards", async () => { + game.modifiers + .addCheck("REVIVE") + .addCheck("MAX_REVIVE") + .addCheck("REVIVER_SEED"); + await game.challengeMode.startBattle(); + + game.move.select(Moves.THUNDERBOLT); + await game.phaseInterceptor.to("SelectModifierPhase"); + game.modifiers + .testCheck("REVIVE", false) + .testCheck("MAX_REVIVE", false) + .testCheck("REVIVER_SEED", false); + }); +}); diff --git a/src/test/battle/inverse_battle.test.ts b/src/test/challenge/inverse_battle.test.ts similarity index 100% rename from src/test/battle/inverse_battle.test.ts rename to src/test/challenge/inverse_battle.test.ts diff --git a/src/test/challenge/limited_catch.test.ts b/src/test/challenge/limited_catch.test.ts new file mode 100644 index 00000000000..39d45ba94bd --- /dev/null +++ b/src/test/challenge/limited_catch.test.ts @@ -0,0 +1,43 @@ +import { CommandPhase } from "#app/phases/command-phase"; +import { Command } from "#app/ui/command-ui-handler"; +import { Challenges } from "#enums/challenges"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Challenge - Limited Catch", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.challengeMode.addChallenge(Challenges.LIMITED_CATCH); + + game.override.battleType("single"); + }); + + it("prevents catching pokemon outside of the first wave of the biome", async () => { + game.override + .startingWave(3) + .pokeballs([ 0, 0, 0, 0, 100 ]); + await game.challengeMode.startBattle([ Species.FEEBAS ]); + + const phase = game.scene.getCurrentPhase() as CommandPhase; + phase.handleCommand(Command.BALL, 4); + await game.phaseInterceptor.to("BattleEndPhase"); + + expect(game.scene.getParty().length).toBe(1); + }); +}); diff --git a/src/test/challenge/no_auto_heal.test.ts b/src/test/challenge/no_auto_heal.test.ts new file mode 100644 index 00000000000..99440df16d7 --- /dev/null +++ b/src/test/challenge/no_auto_heal.test.ts @@ -0,0 +1,55 @@ +import { Abilities } from "#enums/abilities"; +import { Challenges } from "#enums/challenges"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Challenge - No Auto Heal", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.challengeMode.addChallenge(Challenges.NO_AUTO_HEAL); + + game.override + .battleType("single") + .starterSpecies(Species.FEEBAS) + .ability(Abilities.BALL_FETCH) + .moveset(Moves.THUNDERBOLT) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("prevents PartyHealPhase from healing the player's pokemon", async () => { + game.override + .startingWave(10) + .startingLevel(100); + await game.challengeMode.startBattle(); + + const player = game.scene.getPlayerField()[0]; + player.damageAndUpdate(1); + + game.move.select(Moves.THUNDERBOLT); + await game.phaseInterceptor.to("SelectModifierPhase", false); + game.doSelectModifier(); + await game.toNextTurn(); + + expect(player.hp).toBe(player.getMaxHp() - 1); + expect(player.moveset[0]?.ppUsed).toBe(1); + }); +}); diff --git a/src/test/daily_mode.test.ts b/src/test/daily_mode.test.ts index 100cf07f9c0..92e3ad62882 100644 --- a/src/test/daily_mode.test.ts +++ b/src/test/daily_mode.test.ts @@ -1,13 +1,11 @@ -import { MapModifier } from "#app/modifier/modifier"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import GameManager from "./utils/gameManager"; -import { Moves } from "#app/enums/moves"; import { Biome } from "#app/enums/biome"; -import { Mode } from "#app/ui/ui"; +import { Moves } from "#app/enums/moves"; +import { MapModifier } from "#app/modifier/modifier"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { Mode } from "#app/ui/ui"; import { Species } from "#enums/species"; - -//const TIMEOUT = 20 * 1000; +import GameManager from "#test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Daily Mode", () => { let phaserGame: Phaser.Game; diff --git a/src/test/utils/helpers/challengeModeHelper.ts b/src/test/utils/helpers/challengeModeHelper.ts index 5210d942d5a..5c5e8a50d9c 100644 --- a/src/test/utils/helpers/challengeModeHelper.ts +++ b/src/test/utils/helpers/challengeModeHelper.ts @@ -1,16 +1,15 @@ -import { BattleStyle } from "#app/enums/battle-style"; -import { Species } from "#app/enums/species"; +import { Challenge, copyChallenge } from "#app/data/challenge"; import overrides from "#app/overrides"; +import { CommandPhase } from "#app/phases/command-phase"; import { EncounterPhase } from "#app/phases/encounter-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase"; -import { Mode } from "#app/ui/ui"; -import { generateStarter } from "../gameManagerUtils"; -import { GameManagerHelper } from "./gameManagerHelper"; -import { Challenge } from "#app/data/challenge"; -import { CommandPhase } from "#app/phases/command-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { Mode } from "#app/ui/ui"; +import { BattleStyle } from "#enums/battle-style"; import { Challenges } from "#enums/challenges"; -import { copyChallenge } from "data/challenge"; +import { Species } from "#enums/species"; +import { generateStarter } from "#test/utils/gameManagerUtils"; +import { GameManagerHelper } from "#test/utils/helpers/gameManagerHelper"; /** * Helper to handle Challenge mode specifics @@ -25,7 +24,7 @@ export class ChallengeModeHelper extends GameManagerHelper { * @param value - The challenge value. * @param severity - The challenge severity. */ - addChallenge(id: Challenges, value: number, severity: number) { + addChallenge(id: Challenges, value: number = 1, severity: number = 1) { const challenge = copyChallenge({ id, value, severity }); this.challenges.push(challenge); } diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 404f5c34a26..90ebf4019cd 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -1,19 +1,20 @@ import { StatusEffect } from "#app/data/status-effect"; +import { Variant } from "#app/data/variant"; import { Weather, WeatherType } from "#app/data/weather"; -import { Abilities } from "#app/enums/abilities"; -import { Biome } from "#app/enums/biome"; -import { Moves } from "#app/enums/moves"; -import { Species } from "#app/enums/species"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierOverride } from "#app/modifier/modifier-type"; import Overrides from "#app/overrides"; -import { vi } from "vitest"; -import { GameManagerHelper } from "./gameManagerHelper"; import { Unlockables } from "#app/system/unlockables"; -import { Variant } from "#app/data/variant"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Abilities } from "#enums/abilities"; +import { Biome } from "#enums/biome"; +import { Moves } from "#enums/moves"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PokeballType } from "#enums/pokeball"; +import { Species } from "#enums/species"; +import { GameManagerHelper } from "#test/utils/helpers/gameManagerHelper"; +import { vi } from "vitest"; /** * Helper to handle overrides in tests @@ -27,14 +28,14 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the starting biome * @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line - * @param biome the biome to set + * @param biome The {@linkcode Biome} to set + * @returns `this` */ public startingBiome(biome: Biome): this { this.game.scene.newArena(biome); this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`); return this; } - /** * Override the starting wave (index) * @param wave the wave (index) to set. Classic: `1`-`200` @@ -47,8 +48,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player (pokemon) starting level - * @param level the (pokemon) level to set + * Override the player pokemon's starting level + * @param level the level to set * @returns `this` */ public startingLevel(level: Species | number): this { @@ -69,8 +70,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player (pokemon) starting held items - * @param items the items to hold + * Override the player pokemon's starting held items + * @param items the {@linkcode ModifierOverride | items} to hold * @returns `this` */ public startingHeldItems(items: ModifierOverride[]): this { @@ -80,8 +81,35 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player (pokemon) {@linkcode Species | species} - * @param species the (pokemon) {@linkcode Species | species} to set + * Override the player's pokeball inventory + * @param pokeballs Array specifying the amount of each pokeball to set, or `null` if disabling the override + * @param enable Whether to enable or disable the override, default `true` + * @returns `this` + */ + pokeballs(pokeballs: [number, number, number, number, number] | null, enable: boolean = true): this { + if (!pokeballs) { + pokeballs = [ 5, 0, 0, 0, 0 ]; + } + pokeballs = pokeballs!; + const pokeballOverride = { + active: enable, + pokeballs: { + [PokeballType.POKEBALL]: pokeballs[0], + [PokeballType.GREAT_BALL]: pokeballs[1], + [PokeballType.ULTRA_BALL]: pokeballs[2], + [PokeballType.ROGUE_BALL]: pokeballs[3], + [PokeballType.MASTER_BALL]: pokeballs[4], + }, + }; + vi.spyOn(Overrides, "POKEBALL_OVERRIDE", "get").mockReturnValue(pokeballOverride); + this.log(`Pokeball override ${enable ? `set to [${pokeballs}]!` : "disabled!"}`); + return this; + } + + /** + * Override the player pokemon's species. + * It's preferred to use `startBattle([Species.PKMN1, Species.PKMN2, ...])` if possible. + * @param species the {@linkcode Species} to set * @returns `this` */ public starterSpecies(species: Species | number): this { @@ -112,8 +140,15 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player (pokemons) forms - * @param forms the (pokemon) forms to set + * Override the player pokemons' forms + * @param forms the forms to set + * @example + * ```ts + * game.override.starterForms({ + * [Species.KYOGRE]: 1, + * [Species.PIKACHU]: 3, + * }); + * ``` * @returns `this` */ public starterForms(forms: Partial>): this { @@ -127,7 +162,7 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player's starting modifiers - * @param modifiers the modifiers to set + * @param modifiers the {@linkcode ModifierOverride | modifiers} to set * @returns `this` */ public startingModifier(modifiers: ModifierOverride[]): this { @@ -137,8 +172,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player (pokemon) {@linkcode Abilities | ability} - * @param ability the (pokemon) {@linkcode Abilities | ability} to set + * Override the player pokemon's ability + * @param ability the {@linkcode Abilities | ability} to set * @returns `this` */ public ability(ability: Abilities): this { @@ -148,8 +183,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player (pokemon) **passive** {@linkcode Abilities | ability} - * @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set + * Override the player pokemon's **passive** ability + * @param passiveAbility the **passive** {@linkcode Abilities | ability} to set * @returns `this` */ public passiveAbility(passiveAbility: Abilities): this { @@ -159,8 +194,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player (pokemon) {@linkcode Moves | moves}set - * @param moveset the {@linkcode Moves | moves}set to set + * Override the player pokemon's moveset + * @param moveset the {@linkcode Moves | moveset} to set * @returns `this` */ public moveset(moveset: Moves | Moves[]): this { @@ -174,9 +209,9 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player (pokemon) {@linkcode StatusEffect | status-effect} - * @param statusEffect the {@linkcode StatusEffect | status-effect} to set - * @returns + * Override the player pokemon's status effect + * @param statusEffect the {@linkcode StatusEffect | status effect} to set + * @returns `this` */ public statusEffect(statusEffect: StatusEffect): this { vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); @@ -210,8 +245,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the {@linkcode WeatherType | weather (type)} - * @param type {@linkcode WeatherType | weather type} to set + * Override the weather + * @param type The {@linkcode WeatherType} to set * @returns `this` */ public weather(type: WeatherType): this { @@ -237,8 +272,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the battle type (single or double) - * @param battleType battle type to set + * Override the battle type + * @param battleType - The battle type to set ("single" or "double"), `null` to disable * @returns `this` */ public battleType(battleType: "single" | "double" | null): this { @@ -248,8 +283,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy (pokemon) {@linkcode Species | species} - * @param species the (pokemon) {@linkcode Species | species} to set + * Override the enemy pokemon's species + * @param species the {@linkcode Species} to set * @returns `this` */ public enemySpecies(species: Species | number): this { @@ -280,7 +315,7 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy (pokemon) {@linkcode Abilities | ability} + * Override the enemy pokemon's ability * @param ability the (pokemon) {@linkcode Abilities | ability} to set * @returns `this` */ @@ -291,8 +326,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy (pokemon) **passive** {@linkcode Abilities | ability} - * @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set + * Override the enemy pokemon's **passive** ability + * @param passiveAbility the **passive** {@linkcode Abilities | ability} to set * @returns `this` */ public enemyPassiveAbility(passiveAbility: Abilities): this { @@ -302,8 +337,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy (pokemon) {@linkcode Moves | moves}set - * @param moveset the {@linkcode Moves | moves}set to set + * Override the enemy pokemon's moveset + * @param moveset the {@linkcode Moves | moveset} to set * @returns `this` */ public enemyMoveset(moveset: Moves | Moves[]): this { @@ -317,7 +352,7 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy (pokemon) level + * Override the enemy pokemon's level * @param level the level to set * @returns `this` */ @@ -328,8 +363,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy (pokemon) {@linkcode StatusEffect | status-effect} - * @param statusEffect the {@linkcode StatusEffect | status-effect} to set + * Override the enemy pokemon's status effect + * @param statusEffect the {@linkcode StatusEffect} to set * @returns */ public enemyStatusEffect(statusEffect: StatusEffect): this { @@ -339,8 +374,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy (pokemon) held items - * @param items the items to hold + * Override the enemy pokemon's held items + * @param items the {@linkcode ModifierOverride | items} to hold * @returns `this` */ public enemyHeldItems(items: ModifierOverride[]): this { @@ -362,7 +397,7 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the items rolled at the end of a battle - * @param items the items to be rolled + * @param items the {@linkcode ModifierOverride | items} to be rolled * @returns `this` */ public itemRewards(items: ModifierOverride[]): this { @@ -420,7 +455,7 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy (Pokemon) to have the given amount of health segments + * Override the enemy Pokemon to have the given amount of health segments * @param healthSegments the number of segments to give * - `0` (default): the health segments will be handled like in the game based on wave, level and species * - `1`: the Pokemon will not be a boss @@ -462,8 +497,8 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the encounter chance for a mystery encounter. - * @param tier - The {@linkcode MysteryEncounterTier} to encounter + * Override the encounter tier for a mystery encounter. + * @param tier What {@linkcode MysteryEncounterTier | tier} of encounter to set * @returns `this` */ public mysteryEncounterTier(tier: MysteryEncounterTier): this { @@ -474,7 +509,7 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the encounter that spawns for the scene - * @param encounterType - The {@linkcode MysteryEncounterType} of the encounter + * @param encounterType What {@linkcode MysteryEncounterType | type} of encounter to set * @returns `this` */ public mysteryEncounter(encounterType: MysteryEncounterType): this { diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 3f89ebe415f..2e7a3ea5021 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -1,20 +1,20 @@ -import BattleScene from "../battle-scene"; -import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "../modifier/modifier-type"; -import { getPokeballAtlasKey, PokeballType } from "../data/pokeball"; -import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; -import AwaitableUiHandler from "./awaitable-ui-handler"; -import { Mode } from "./ui"; -import { LockModifierTiersModifier, PokemonHeldItemModifier, HealShopCostModifier } from "../modifier/modifier"; -import { handleTutorial, Tutorial } from "../tutorial"; -import { Button } from "#enums/buttons"; -import MoveInfoOverlay from "./move-info-overlay"; -import { allMoves } from "../data/move"; -import * as Utils from "./../utils"; +import BattleScene from "#app/battle-scene"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { allMoves } from "#app/data/move"; +import { getPokeballAtlasKey, PokeballType } from "#app/data/pokeball"; +import { HealShopCostModifier, LockModifierTiersModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "#app/modifier/modifier-type"; import Overrides from "#app/overrides"; +import { handleTutorial, Tutorial } from "#app/tutorial"; +import { BooleanHolder, formatMoney, NumberHolder } from "#app/utils"; +import { Button } from "#enums/buttons"; +import { ShopCursorTarget } from "#enums/shop-cursor-target"; import i18next from "i18next"; -import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; -import { IntegerHolder } from "./../utils"; import Phaser from "phaser"; +import AwaitableUiHandler from "./awaitable-ui-handler"; +import MoveInfoOverlay from "./move-info-overlay"; +import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions, TextStyle } from "./text"; +import { Mode } from "./ui"; export const SHOP_OPTIONS_ROW_LIMIT = 7; const SINGLE_SHOP_ROW_YOFFSET = 12; @@ -189,10 +189,16 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { const typeOptions = args[1] as ModifierTypeOption[]; const removeHealShop = this.scene.gameMode.hasNoShop; - const baseShopCost = new IntegerHolder(this.scene.getWaveMoneyAmount(1)); + const baseShopCost = new NumberHolder(this.scene.getWaveMoneyAmount(1)); + this.scene.applyModifier(HealShopCostModifier, true, baseShopCost); + const shopTypeOptions = !removeHealShop - ? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value) + ? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value, this.scene.gameMode).filter(shopItem => { + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(this.scene.gameMode, 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; @@ -559,7 +565,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { } const canReroll = this.scene.money >= this.rerollCost; - const formattedMoney = Utils.formatMoney(this.scene.moneyFormat, this.rerollCost); + const formattedMoney = formatMoney(this.scene.moneyFormat, this.rerollCost); this.rerollCostText.setText(i18next.t("modifierSelectUiHandler:rerollCost", { formattedMoney })); this.rerollCostText.setColor(this.getTextColor(canReroll ? TextStyle.MONEY : TextStyle.PARTY_RED)); @@ -828,7 +834,7 @@ class ModifierOption extends Phaser.GameObjects.Container { const cost = Overrides.WAIVE_ROLL_FEE_OVERRIDE ? 0 : this.modifierTypeOption.cost; const textStyle = cost <= scene.money ? TextStyle.MONEY : TextStyle.PARTY_RED; - const formattedMoney = Utils.formatMoney(scene.moneyFormat, cost); + const formattedMoney = formatMoney(scene.moneyFormat, cost); this.itemCostText.setText(i18next.t("modifierSelectUiHandler:itemCost", { formattedMoney })); this.itemCostText.setColor(getTextColor(textStyle, false, scene.uiTheme));