mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-12-15 06:15:20 +01:00
* Added `noBannedTypes` as a biome rule * Added `useShorthandAssign` rule * Added `useConsistentArrayType` * Update src/field/pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/pokeball.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Apply Biome after merge --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
1280 lines
42 KiB
TypeScript
1280 lines
42 KiB
TypeScript
import type { FixedBattleConfig } from "#app/battle";
|
|
import { getRandomTrainerFunc } from "#app/battle";
|
|
import { globalScene } from "#app/global-scene";
|
|
import { defaultStarterSpeciesAndEvolutions } from "#balance/pokemon-evolutions";
|
|
import { speciesStarterCosts } from "#balance/starters";
|
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
|
import { AbilityAttr } from "#enums/ability-attr";
|
|
import { BattleType } from "#enums/battle-type";
|
|
import { Challenges } from "#enums/challenges";
|
|
import { TypeColor, TypeShadow } from "#enums/color";
|
|
import { DexAttr } from "#enums/dex-attr";
|
|
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
|
import { ModifierTier } from "#enums/modifier-tier";
|
|
import { MoveId } from "#enums/move-id";
|
|
import type { MoveSourceType } from "#enums/move-source-type";
|
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
|
import { Nature } from "#enums/nature";
|
|
import { PokemonType } from "#enums/pokemon-type";
|
|
import { SpeciesId } from "#enums/species-id";
|
|
import { TrainerType } from "#enums/trainer-type";
|
|
import { TrainerVariant } from "#enums/trainer-variant";
|
|
import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
|
|
import { Trainer } from "#field/trainer";
|
|
import type { ModifierTypeOption } from "#modifiers/modifier-type";
|
|
import { PokemonMove } from "#moves/pokemon-move";
|
|
import type { GameData } from "#system/game-data";
|
|
import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data";
|
|
import type { DexEntry } from "#types/dex-data";
|
|
import type { DexAttrProps, StarterDataEntry } from "#types/save-data";
|
|
import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
|
|
import { deepCopy } from "#utils/data";
|
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
|
import { toCamelCase } from "#utils/strings";
|
|
import i18next from "i18next";
|
|
|
|
/** A constant for the default max cost of the starting party before a run */
|
|
const DEFAULT_PARTY_MAX_COST = 10;
|
|
|
|
/**
|
|
* 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: number; // The "strength" of the challenge, all challenges have a numerical value.
|
|
public maxValue: number; // The maximum strength of the challenge.
|
|
public severity: number; // The current severity of the challenge. Some challenges have multiple severities in addition to strength.
|
|
public maxSeverity: number; // The maximum severity of the challenge.
|
|
|
|
public conditions: ChallengeCondition[];
|
|
|
|
/**
|
|
* The Ribbon awarded on challenge completion, or 0 if the challenge has no ribbon or is not enabled
|
|
*
|
|
* @defaultValue 0
|
|
*/
|
|
public get ribbonAwarded(): RibbonFlag {
|
|
return 0n as RibbonFlag;
|
|
}
|
|
|
|
/**
|
|
* @param id - The enum value for the challenge
|
|
*/
|
|
constructor(id: Challenges, maxValue: number = Number.MAX_SAFE_INTEGER) {
|
|
this.id = id;
|
|
|
|
this.value = 0;
|
|
this.maxValue = maxValue;
|
|
this.severity = 0;
|
|
this.maxSeverity = 0;
|
|
this.conditions = [];
|
|
}
|
|
|
|
/**
|
|
* Reset the challenge to a base state.
|
|
*/
|
|
reset(): void {
|
|
this.value = 0;
|
|
this.severity = 0;
|
|
}
|
|
|
|
/**
|
|
* Gets the localization key for the challenge
|
|
* @returns The i18n key for this challenge as camel case.
|
|
*/
|
|
geti18nKey(): string {
|
|
return toCamelCase(Challenges[this.id]);
|
|
}
|
|
|
|
/**
|
|
* Check if an unlockable challenge is unlocked
|
|
* @param data - The save data
|
|
* @returns Whether this challenge is unlocked
|
|
*/
|
|
isUnlocked(data: GameData): boolean {
|
|
return this.conditions.every(f => f(data));
|
|
}
|
|
|
|
/**
|
|
* Adds an unlock condition to this challenge.
|
|
* @param condition - The condition to add
|
|
* @returns This challenge
|
|
*/
|
|
condition(condition: ChallengeCondition): Challenge {
|
|
this.conditions.push(condition);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* @returns The localised name of this challenge.
|
|
*/
|
|
getName(): string {
|
|
return i18next.t(`challenges:${this.geti18nKey()}.name`);
|
|
}
|
|
|
|
/**
|
|
* Return the textual representation of a challenge's current value.
|
|
* @param overrideValue - The value to check for; default {@linkcode this.value}
|
|
* @returns The localised text for the current value.
|
|
*/
|
|
getValue(overrideValue: number = this.value): string {
|
|
return i18next.t(`challenges:${this.geti18nKey()}.value.${overrideValue}`);
|
|
}
|
|
|
|
/**
|
|
* Return the description of a challenge's current value.
|
|
* @param overrideValue - The value to check for; default {@linkcode this.value}
|
|
* @returns The localised description for the current value.
|
|
*/
|
|
// TODO: Do we need an override value here? it's currently unused
|
|
getDescription(overrideValue: number = this.value): string {
|
|
return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${overrideValue}`, `challenges:${this.geti18nKey()}.desc`])}`;
|
|
}
|
|
|
|
/**
|
|
* Increase the value of the challenge
|
|
* @returns Returns true if the value changed
|
|
*/
|
|
increaseValue(): boolean {
|
|
if (this.value < this.maxValue) {
|
|
this.value = Math.min(this.value + 1, this.maxValue);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Decrease the value of the challenge
|
|
* @returns Returns true if the value changed
|
|
*/
|
|
decreaseValue(): boolean {
|
|
if (this.value > 0) {
|
|
this.value = Math.max(this.value - 1, 0);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Whether to allow choosing this challenge's severity.
|
|
*/
|
|
hasSeverity(): boolean {
|
|
return this.value !== 0 && this.maxSeverity > 0;
|
|
}
|
|
|
|
/**
|
|
* Decrease the severity of the challenge
|
|
* @returns Returns true if the value changed
|
|
*/
|
|
decreaseSeverity(): boolean {
|
|
if (this.severity > 0) {
|
|
this.severity = Math.max(this.severity - 1, 0);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Increase the severity of the challenge
|
|
* @returns Returns true if the value changed
|
|
*/
|
|
increaseSeverity(): boolean {
|
|
if (this.severity < this.maxSeverity) {
|
|
this.severity = Math.min(this.severity + 1, this.maxSeverity);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gets the "difficulty" value of this challenge.
|
|
* @returns The difficulty value.
|
|
*/
|
|
getDifficulty(): number {
|
|
return this.value;
|
|
}
|
|
|
|
/**
|
|
* Gets the minimum difficulty added by this challenge.
|
|
* @returns The difficulty value.
|
|
*/
|
|
getMinDifficulty(): number {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Clones a challenge, either from another challenge or json. Chainable.
|
|
* @param _source - The source challenge or json.
|
|
* @returns This 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 - The Pokémon to check the validity of
|
|
* @param _valid - Holder for whether the Pokémon is valid or not
|
|
* @param _dexAttr - The dex attributes of the Pokémon
|
|
* @returns Whether this function did anything.
|
|
*/
|
|
applyStarterChoice(_pokemon: PokemonSpecies, _valid: BooleanHolder, _dexAttr: DexAttrProps): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for STARTER_POINTS challenges. Derived classes should alter this.
|
|
* @param _points - Holder for amount of starter points the user has to spend
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyStarterPoints(_points: NumberHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for STARTER_COST challenges. Derived classes should alter this.
|
|
* @param _species - The pokémon to change the cost of
|
|
* @param _cost - Holder for the cost of the starter Pokémon
|
|
* @returns Whether this function did anything.
|
|
*/
|
|
applyStarterCost(_species: SpeciesId, _cost: NumberHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for STARTER_SELECT_MODIFY challenges. Derived classes should alter this.
|
|
* @param _pokemon - The starter Pokémon to modify.
|
|
* @returns Whether this function did anything.
|
|
*/
|
|
applyStarterSelectModify(_speciesId: SpeciesId, _dexEntry: DexEntry, _starterDataEntry: StarterDataEntry): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for STARTER_MODIFY challenges. Derived classes should alter this.
|
|
* @param _pokemon - The starter Pokémon to modify.
|
|
* @returns Whether 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 - The Pokémon to check the validity of
|
|
* @param _valid - Holds a boolean that will be set to false if the Pokémon isn't allowed
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyPokemonInBattle(_pokemon: Pokemon, _valid: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for FIXED_BATTLE challenges. Derived classes should alter this.
|
|
* @param _waveIndex The current wave index
|
|
* @param _battleConfig - The battle config to modify
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyFixedBattle(_waveIndex: number, _battleConfig: FixedBattleConfig): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for TYPE_EFFECTIVENESS challenges. Derived classes should alter this.
|
|
* @param _effectiveness - The current effectiveness of the move
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyTypeEffectiveness(_effectiveness: NumberHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for AI_LEVEL challenges. Derived classes should alter this.
|
|
* @param _level - The generated level.
|
|
* @param _levelCap - The current level cap.
|
|
* @param _isTrainer - Whether this is a trainer Pokémon
|
|
* @param _isBoss - Whether this is a non-trainer boss Pokémon
|
|
* @returns - Whether this function did anything
|
|
*/
|
|
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 - The Pokémon that is being considered
|
|
* @param _moveSlots - The amount of move slots
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyMoveSlot(_pokemon: Pokemon, _moveSlots: NumberHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for PASSIVE_ACCESS challenges. Derived classes should alter this.
|
|
* @param _pokemon - The Pokémon to change
|
|
* @param _hasPassive - Whether it should have its passive
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyPassiveAccess(_pokemon: Pokemon, _hasPassive: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for GAME_MODE_MODIFY challenges. Derived classes should alter this.
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyGameModeModify(): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for MOVE_ACCESS. Derived classes should alter this.
|
|
* @param _pokemon - What Pokémon would learn the move
|
|
* @param _moveSource - What source the Pokémon would get the move from
|
|
* @param _move - The move in question
|
|
* @param _level - The level threshold for access
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyMoveAccessLevel(_pokemon: Pokemon, _moveSource: MoveSourceType, _move: MoveId, _level: NumberHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for MOVE_WEIGHT. Derived classes should alter this.
|
|
* @param _pokemon - What Pokémon would learn the move
|
|
* @param _moveSource - What source the Pokémon would get the move from
|
|
* @param _move - The move in question.
|
|
* @param _weight - The base weight of the move
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyMoveWeight(_pokemon: Pokemon, _moveSource: MoveSourceType, _move: MoveId, _weight: NumberHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for FlipStats. Derived classes should alter this.
|
|
* @param _pokemon - What Pokémon would learn the move
|
|
* @param _baseStats What are the stats to flip
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for PARTY_HEAL. Derived classes should alter this.
|
|
* @param _status - Whether party healing is enabled or not
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyPartyHeal(_status: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for SHOP. Derived classes should alter this.
|
|
* @param _status - Whether the shop is or is not available after a wave
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyShop(_status: BooleanHolder) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for POKEMON_ADD_TO_PARTY. Derived classes should alter this.
|
|
* @param _pokemon - The Pokémon being caught
|
|
* @param _status - Whether the Pokémon can be added to the party or not
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyPokemonAddToParty(_pokemon: EnemyPokemon, _status: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for POKEMON_FUSION. Derived classes should alter this.
|
|
* @param _pokemon - The Pokémon being checked
|
|
* @param _status - Whether the selected Pokémon is allowed to fuse or not
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyPokemonFusion(_pokemon: PlayerPokemon, _status: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for POKEMON_MOVE. Derived classes should alter this.
|
|
* @param _moveId - The {@linkcode MoveId} being checked
|
|
* @param _status - A {@linkcode BooleanHolder} containing the move's usability status
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyPokemonMove(_moveId: MoveId, _status: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for SHOP_ITEM. Derived classes should alter this.
|
|
* @param _shopItem - The item being checked
|
|
* @param _status - Whether the item should be added to the shop or not
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyShopItem(_shopItem: ModifierTypeOption | null, _status: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for WAVE_REWARD. Derived classes should alter this.
|
|
* @param _reward - The reward being checked
|
|
* @param _status - Whether the reward should be added to the reward options or not
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyWaveReward(_reward: ModifierTypeOption | null, _status: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* An apply function for PREVENT_REVIVE. Derived classes should alter this.
|
|
* @param _status - Whether fainting is a permanent status or not
|
|
* @returns Whether this function did anything
|
|
*/
|
|
applyPreventRevive(_status: BooleanHolder): boolean {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
type ChallengeCondition = (data: GameData) => boolean;
|
|
|
|
/**
|
|
* Implements a mono generation challenge.
|
|
*/
|
|
export class SingleGenerationChallenge extends Challenge {
|
|
public override get ribbonAwarded(): RibbonFlag {
|
|
// NOTE: This logic will not work for the eventual mono gen 10 ribbon, as
|
|
// as its flag will not be in sequence with the other mono gen ribbons.
|
|
return this.value ? ((RibbonData.MONO_GEN_1 << (BigInt(this.value) - 1n)) as RibbonFlag) : 0n;
|
|
}
|
|
|
|
constructor() {
|
|
super(Challenges.SINGLE_GENERATION, 9);
|
|
}
|
|
|
|
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
|
|
if (pokemon.generation !== this.value) {
|
|
valid.value = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
applyStarterSelectModify(speciesId: SpeciesId, dexEntry: DexEntry, _starterDataEntry: StarterDataEntry): boolean {
|
|
// Ralts must be male and Snorunt must be female
|
|
if (this.value === 4) {
|
|
if (speciesId === SpeciesId.RALTS) {
|
|
dexEntry.caughtAttr &= ~DexAttr.FEMALE;
|
|
}
|
|
if (speciesId === SpeciesId.SNORUNT) {
|
|
dexEntry.caughtAttr &= ~DexAttr.MALE;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
applyPokemonInBattle(pokemon: Pokemon, valid: BooleanHolder): boolean {
|
|
const baseGeneration = getPokemonSpecies(pokemon.species.speciesId).generation;
|
|
const fusionGeneration = pokemon.isFusion() ? getPokemonSpecies(pokemon.fusionSpecies!.speciesId).generation : 0;
|
|
if (
|
|
pokemon.isPlayer()
|
|
&& (baseGeneration !== this.value || (pokemon.isFusion() && fusionGeneration !== this.value))
|
|
) {
|
|
valid.value = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
applyFixedBattle(waveIndex: number, battleConfig: FixedBattleConfig): boolean {
|
|
let trainerTypes: (TrainerType | TrainerType[])[] = [];
|
|
const evilTeamWaves: number[] = [
|
|
ClassicFixedBossWaves.EVIL_GRUNT_1,
|
|
ClassicFixedBossWaves.EVIL_GRUNT_2,
|
|
ClassicFixedBossWaves.EVIL_GRUNT_3,
|
|
ClassicFixedBossWaves.EVIL_ADMIN_1,
|
|
ClassicFixedBossWaves.EVIL_GRUNT_4,
|
|
ClassicFixedBossWaves.EVIL_ADMIN_2,
|
|
ClassicFixedBossWaves.EVIL_BOSS_1,
|
|
ClassicFixedBossWaves.EVIL_ADMIN_3,
|
|
ClassicFixedBossWaves.EVIL_BOSS_2,
|
|
];
|
|
const evilTeamGrunts = [
|
|
[TrainerType.ROCKET_GRUNT],
|
|
[TrainerType.ROCKET_GRUNT],
|
|
[TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT],
|
|
[TrainerType.GALACTIC_GRUNT],
|
|
[TrainerType.PLASMA_GRUNT],
|
|
[TrainerType.FLARE_GRUNT],
|
|
[TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT],
|
|
[TrainerType.MACRO_GRUNT],
|
|
[TrainerType.STAR_GRUNT],
|
|
];
|
|
const evilAdminFight1 = [
|
|
[TrainerType.PETREL],
|
|
[TrainerType.PETREL],
|
|
[
|
|
[TrainerType.TABITHA, TrainerType.COURTNEY],
|
|
[TrainerType.MATT, TrainerType.SHELLY],
|
|
],
|
|
[TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN],
|
|
[TrainerType.COLRESS],
|
|
[TrainerType.BRYONY],
|
|
[TrainerType.FABA, TrainerType.PLUMERIA],
|
|
[TrainerType.OLEANA],
|
|
[TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI],
|
|
];
|
|
const evilAdminFight2 = [
|
|
[TrainerType.PROTON],
|
|
[TrainerType.PROTON],
|
|
[
|
|
[TrainerType.TABITHA, TrainerType.COURTNEY],
|
|
[TrainerType.MATT, TrainerType.SHELLY],
|
|
],
|
|
[TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN],
|
|
[TrainerType.ZINZOLIN],
|
|
[TrainerType.BRYONY, TrainerType.XEROSIC],
|
|
[TrainerType.FABA, TrainerType.PLUMERIA],
|
|
[TrainerType.OLEANA],
|
|
[TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI],
|
|
];
|
|
const evilAdminFight3 = [
|
|
[TrainerType.ARCHER, TrainerType.ARIANA],
|
|
[TrainerType.ARCHER, TrainerType.ARIANA],
|
|
[
|
|
[TrainerType.TABITHA, TrainerType.COURTNEY],
|
|
[TrainerType.MATT, TrainerType.SHELLY],
|
|
],
|
|
[TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN],
|
|
[TrainerType.COLRESS],
|
|
[TrainerType.XEROSIC],
|
|
[TrainerType.FABA, TrainerType.PLUMERIA],
|
|
[TrainerType.OLEANA],
|
|
[TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI],
|
|
];
|
|
const evilTeamBosses = [
|
|
[TrainerType.ROCKET_BOSS_GIOVANNI_1],
|
|
[TrainerType.ROCKET_BOSS_GIOVANNI_1],
|
|
[TrainerType.MAXIE, TrainerType.ARCHIE],
|
|
[TrainerType.CYRUS],
|
|
[TrainerType.GHETSIS],
|
|
[TrainerType.LYSANDRE],
|
|
[TrainerType.LUSAMINE, TrainerType.GUZMA],
|
|
[TrainerType.ROSE],
|
|
[TrainerType.PENNY],
|
|
];
|
|
const evilTeamBossRematches = [
|
|
[TrainerType.ROCKET_BOSS_GIOVANNI_2],
|
|
[TrainerType.ROCKET_BOSS_GIOVANNI_2],
|
|
[TrainerType.MAXIE_2, TrainerType.ARCHIE_2],
|
|
[TrainerType.CYRUS_2],
|
|
[TrainerType.GHETSIS_2],
|
|
[TrainerType.LYSANDRE_2],
|
|
[TrainerType.LUSAMINE_2, TrainerType.GUZMA_2],
|
|
[TrainerType.ROSE_2],
|
|
[TrainerType.PENNY_2],
|
|
];
|
|
switch (waveIndex) {
|
|
case ClassicFixedBossWaves.EVIL_GRUNT_1:
|
|
trainerTypes = evilTeamGrunts[this.value - 1];
|
|
battleConfig.setBattleType(BattleType.TRAINER).setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true));
|
|
return true;
|
|
case ClassicFixedBossWaves.EVIL_GRUNT_2:
|
|
case ClassicFixedBossWaves.EVIL_GRUNT_3:
|
|
case ClassicFixedBossWaves.EVIL_GRUNT_4:
|
|
trainerTypes = evilTeamGrunts[this.value - 1];
|
|
break;
|
|
case ClassicFixedBossWaves.EVIL_ADMIN_1:
|
|
trainerTypes = evilAdminFight1[this.value - 1];
|
|
break;
|
|
case ClassicFixedBossWaves.EVIL_ADMIN_2:
|
|
trainerTypes = evilAdminFight2[this.value - 1];
|
|
break;
|
|
case ClassicFixedBossWaves.EVIL_ADMIN_3:
|
|
trainerTypes = evilAdminFight3[this.value - 1];
|
|
break;
|
|
case ClassicFixedBossWaves.EVIL_BOSS_1:
|
|
trainerTypes = evilTeamBosses[this.value - 1];
|
|
battleConfig
|
|
.setBattleType(BattleType.TRAINER)
|
|
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
|
|
.setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true))
|
|
.setCustomModifierRewards({
|
|
guaranteedModifierTiers: [
|
|
ModifierTier.ROGUE,
|
|
ModifierTier.ROGUE,
|
|
ModifierTier.ULTRA,
|
|
ModifierTier.ULTRA,
|
|
ModifierTier.ULTRA,
|
|
],
|
|
allowLuckUpgrades: false,
|
|
});
|
|
return true;
|
|
case ClassicFixedBossWaves.EVIL_BOSS_2:
|
|
trainerTypes = evilTeamBossRematches[this.value - 1];
|
|
battleConfig
|
|
.setBattleType(BattleType.TRAINER)
|
|
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
|
|
.setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true))
|
|
.setCustomModifierRewards({
|
|
guaranteedModifierTiers: [
|
|
ModifierTier.ROGUE,
|
|
ModifierTier.ROGUE,
|
|
ModifierTier.ULTRA,
|
|
ModifierTier.ULTRA,
|
|
ModifierTier.ULTRA,
|
|
ModifierTier.ULTRA,
|
|
],
|
|
allowLuckUpgrades: false,
|
|
});
|
|
return true;
|
|
case ClassicFixedBossWaves.ELITE_FOUR_1:
|
|
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 ClassicFixedBossWaves.ELITE_FOUR_2:
|
|
trainerTypes = [
|
|
TrainerType.BRUNO,
|
|
TrainerType.KOGA,
|
|
TrainerType.PHOEBE,
|
|
TrainerType.BERTHA,
|
|
TrainerType.MARSHAL,
|
|
TrainerType.SIEBOLD,
|
|
TrainerType.OLIVIA,
|
|
TrainerType.NESSA_ELITE,
|
|
TrainerType.POPPY,
|
|
];
|
|
break;
|
|
case ClassicFixedBossWaves.ELITE_FOUR_3:
|
|
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 ClassicFixedBossWaves.ELITE_FOUR_4:
|
|
trainerTypes = [
|
|
TrainerType.LANCE,
|
|
TrainerType.KAREN,
|
|
TrainerType.DRAKE,
|
|
TrainerType.LUCIAN,
|
|
TrainerType.CAITLIN,
|
|
TrainerType.DRASNA,
|
|
TrainerType.KAHILI,
|
|
TrainerType.RAIHAN_ELITE,
|
|
TrainerType.HASSEL,
|
|
];
|
|
break;
|
|
case ClassicFixedBossWaves.CHAMPION:
|
|
trainerTypes = [
|
|
TrainerType.BLUE,
|
|
randSeedItem([TrainerType.RED, TrainerType.LANCE_CHAMPION]),
|
|
randSeedItem([TrainerType.STEVEN, TrainerType.WALLACE]),
|
|
TrainerType.CYNTHIA,
|
|
randSeedItem([TrainerType.ALDER, TrainerType.IRIS]),
|
|
TrainerType.DIANTHA,
|
|
randSeedItem([TrainerType.KUKUI, TrainerType.HAU]),
|
|
randSeedItem([TrainerType.LEON, TrainerType.MUSTARD]),
|
|
randSeedItem([TrainerType.GEETA, TrainerType.NEMONA]),
|
|
];
|
|
break;
|
|
}
|
|
if (trainerTypes.length === 0) {
|
|
return false;
|
|
}
|
|
if (evilTeamWaves.includes(waveIndex)) {
|
|
battleConfig
|
|
.setBattleType(BattleType.TRAINER)
|
|
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
|
|
.setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true));
|
|
return true;
|
|
}
|
|
if (waveIndex >= ClassicFixedBossWaves.ELITE_FOUR_1 && waveIndex <= ClassicFixedBossWaves.CHAMPION) {
|
|
const ttypes = trainerTypes as TrainerType[];
|
|
battleConfig
|
|
.setBattleType(BattleType.TRAINER)
|
|
.setGetTrainerFunc(() => new Trainer(ttypes[this.value - 1], TrainerVariant.DEFAULT));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
override getDifficulty(): number {
|
|
return this.value > 0 ? 1 : 0;
|
|
}
|
|
|
|
getValue(overrideValue: number = this.value): string {
|
|
if (overrideValue === 0) {
|
|
return i18next.t("settings:off");
|
|
}
|
|
return i18next.t(`starterSelectUiHandler:gen${overrideValue}`);
|
|
}
|
|
|
|
getDescription(overrideValue: number = this.value): string {
|
|
if (overrideValue === 0) {
|
|
return i18next.t("challenges:singleGeneration.descDefault");
|
|
}
|
|
return i18next.t("challenges:singleGeneration.desc", {
|
|
gen: i18next.t(`challenges:singleGeneration.gen.${overrideValue}`),
|
|
});
|
|
}
|
|
|
|
static loadChallenge(source: SingleGenerationChallenge | any): SingleGenerationChallenge {
|
|
const newChallenge = new SingleGenerationChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
interface monotypeOverride {
|
|
/** The species to override */
|
|
species: SpeciesId;
|
|
/** The type to count as */
|
|
type: PokemonType;
|
|
/** If part of a fusion, should we check the fused species instead of the base species? */
|
|
fusion: boolean;
|
|
}
|
|
|
|
/**
|
|
* Implements a mono type challenge.
|
|
*/
|
|
export class SingleTypeChallenge extends Challenge {
|
|
public override get ribbonAwarded(): RibbonFlag {
|
|
// `this.value` represents the 1-based index of pokemon type
|
|
// `RibbonData.MONO_NORMAL` starts the flag position for the types,
|
|
// and we shift it by 1 for the specific type.
|
|
return this.value ? ((RibbonData.MONO_NORMAL << (BigInt(this.value) - 1n)) as RibbonFlag) : 0n;
|
|
}
|
|
private static TYPE_OVERRIDES: monotypeOverride[] = [
|
|
{ species: SpeciesId.CASTFORM, type: PokemonType.NORMAL, fusion: false },
|
|
];
|
|
// TODO: Find a solution for all Pokemon with this ssui issue, including Basculin and Burmy
|
|
|
|
constructor() {
|
|
super(Challenges.SINGLE_TYPE, 18);
|
|
}
|
|
|
|
override applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder, dexAttr: DexAttrProps): boolean {
|
|
const speciesForm = getPokemonSpeciesForm(pokemon.speciesId, dexAttr.formIndex);
|
|
const types = [speciesForm.type1, speciesForm.type2];
|
|
if (!types.includes(this.value - 1)) {
|
|
valid.value = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
applyStarterSelectModify(speciesId: SpeciesId, dexEntry: DexEntry, _starterDataEntry: StarterDataEntry): boolean {
|
|
const type = this.value - 1;
|
|
|
|
if (speciesId === SpeciesId.RALTS) {
|
|
if (type === PokemonType.FIGHTING) {
|
|
dexEntry.caughtAttr &= ~DexAttr.FEMALE;
|
|
}
|
|
if (type === PokemonType.FAIRY) {
|
|
dexEntry.caughtAttr &= ~DexAttr.MALE;
|
|
}
|
|
}
|
|
if (speciesId === SpeciesId.SNORUNT && type === PokemonType.GHOST) {
|
|
dexEntry.caughtAttr &= ~DexAttr.MALE;
|
|
}
|
|
if (speciesId === SpeciesId.BURMY) {
|
|
if (type === PokemonType.FLYING) {
|
|
dexEntry.caughtAttr &= ~DexAttr.FEMALE;
|
|
}
|
|
if ([PokemonType.GRASS, PokemonType.GROUND, PokemonType.STEEL].includes(type)) {
|
|
dexEntry.caughtAttr &= ~DexAttr.MALE;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
override getDifficulty(): number {
|
|
return this.value > 0 ? 1 : 0;
|
|
}
|
|
|
|
getValue(overrideValue: number = this.value): string {
|
|
return PokemonType[overrideValue - 1].toLowerCase();
|
|
}
|
|
|
|
getDescription(overrideValue: number = this.value): string {
|
|
const type = i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[overrideValue - 1])}`);
|
|
const typeColor = `[color=${TypeColor[PokemonType[overrideValue - 1]]}][shadow=${TypeShadow[PokemonType[this.value - 1]]}]${type}[/shadow][/color]`;
|
|
const defaultDesc = i18next.t("challenges:singleType.descDefault");
|
|
const typeDesc = i18next.t("challenges:singleType.desc", {
|
|
type: typeColor,
|
|
});
|
|
return this.value === 0 ? defaultDesc : typeDesc;
|
|
}
|
|
|
|
static loadChallenge(source: SingleTypeChallenge | any): SingleTypeChallenge {
|
|
const newChallenge = new SingleTypeChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements a fresh start challenge.
|
|
*/
|
|
export class FreshStartChallenge extends Challenge {
|
|
public override get ribbonAwarded(): RibbonFlag {
|
|
return this.value ? RibbonData.FRESH_START : 0n;
|
|
}
|
|
constructor() {
|
|
super(Challenges.FRESH_START, 2);
|
|
}
|
|
|
|
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
|
|
if (this.value === 1 && !defaultStarterSpeciesAndEvolutions.includes(pokemon.speciesId)) {
|
|
valid.value = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
applyStarterCost(species: SpeciesId, cost: NumberHolder): boolean {
|
|
cost.value = speciesStarterCosts[species];
|
|
return true;
|
|
}
|
|
|
|
applyStarterSelectModify(speciesId: SpeciesId, dexEntry: DexEntry, starterDataEntry: StarterDataEntry): boolean {
|
|
// Remove all egg moves
|
|
starterDataEntry.eggMoves = 0;
|
|
|
|
// Remove hidden and passive ability
|
|
const defaultAbilities = AbilityAttr.ABILITY_1 | AbilityAttr.ABILITY_2;
|
|
starterDataEntry.abilityAttr &= defaultAbilities;
|
|
starterDataEntry.passiveAttr = 0;
|
|
|
|
// Remove cost reduction
|
|
starterDataEntry.valueReduction = 0;
|
|
|
|
// Remove natures except for the default ones
|
|
const neutralNaturesAttr =
|
|
(1 << (Nature.HARDY + 1))
|
|
| (1 << (Nature.DOCILE + 1))
|
|
| (1 << (Nature.SERIOUS + 1))
|
|
| (1 << (Nature.BASHFUL + 1))
|
|
| (1 << (Nature.QUIRKY + 1));
|
|
dexEntry.natureAttr &= neutralNaturesAttr;
|
|
|
|
// Cap all ivs at 15
|
|
for (let i = 0; i < 6; i++) {
|
|
dexEntry.ivs[i] = Math.min(dexEntry.ivs[i], 15);
|
|
}
|
|
|
|
// Removes shiny and variants
|
|
dexEntry.caughtAttr &= ~DexAttr.SHINY;
|
|
dexEntry.caughtAttr &= ~(DexAttr.VARIANT_2 | DexAttr.VARIANT_3);
|
|
|
|
// Remove unlocked forms for specific species
|
|
if (speciesId === SpeciesId.ZYGARDE) {
|
|
// Sets ability from power construct to aura break
|
|
const formMask = (DexAttr.DEFAULT_FORM << 2n) - 1n;
|
|
dexEntry.caughtAttr &= formMask;
|
|
} else if (
|
|
[
|
|
SpeciesId.PIKACHU,
|
|
SpeciesId.EEVEE,
|
|
SpeciesId.PICHU,
|
|
SpeciesId.ROTOM,
|
|
SpeciesId.MELOETTA,
|
|
SpeciesId.FROAKIE,
|
|
].includes(speciesId)
|
|
) {
|
|
const formMask = (DexAttr.DEFAULT_FORM << 1n) - 1n; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes
|
|
dexEntry.caughtAttr &= formMask;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
applyStarterModify(pokemon: Pokemon): boolean {
|
|
pokemon.abilityIndex %= 2; // Always base ability, if you set it to hidden it wraps to first ability
|
|
pokemon.passive = false; // Passive isn't unlocked
|
|
let validMoves = pokemon.species
|
|
.getLevelMoves()
|
|
.filter(m => isBetween(m[0], 1, 5))
|
|
.map(lm => lm[1]);
|
|
// Filter egg moves out of the moveset
|
|
pokemon.moveset = pokemon.moveset.filter(pm => validMoves.includes(pm.moveId));
|
|
if (pokemon.moveset.length < 4) {
|
|
// If there's empty slots fill with remaining valid moves
|
|
const existingMoveIds = pokemon.moveset.map(pm => pm.moveId);
|
|
validMoves = validMoves.filter(m => !existingMoveIds.includes(m));
|
|
pokemon.moveset = pokemon.moveset.concat(validMoves.map(m => new PokemonMove(m))).slice(0, 4);
|
|
}
|
|
pokemon.luck = 0; // No luck
|
|
pokemon.shiny = false; // Not shiny
|
|
pokemon.variant = 0; // Not shiny
|
|
if (pokemon.species.speciesId === SpeciesId.ZYGARDE && pokemon.formIndex >= 2) {
|
|
pokemon.formIndex -= 2; // Sets 10%-PC to 10%-AB and 50%-PC to 50%-AB
|
|
} else if (
|
|
pokemon.formIndex > 0
|
|
&& [
|
|
SpeciesId.PIKACHU,
|
|
SpeciesId.EEVEE,
|
|
SpeciesId.PICHU,
|
|
SpeciesId.ROTOM,
|
|
SpeciesId.MELOETTA,
|
|
SpeciesId.FROAKIE,
|
|
].includes(pokemon.species.speciesId)
|
|
) {
|
|
pokemon.formIndex = 0; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes
|
|
}
|
|
// Cap all ivs at 15
|
|
for (let i = 0; i < 6; i++) {
|
|
pokemon.ivs[i] = Math.min(pokemon.ivs[i], 15);
|
|
}
|
|
pokemon.teraType = pokemon.species.type1; // Always primary tera type
|
|
return true;
|
|
}
|
|
|
|
override getDifficulty(): number {
|
|
return 0;
|
|
}
|
|
|
|
static loadChallenge(source: FreshStartChallenge | any): FreshStartChallenge {
|
|
const newChallenge = new FreshStartChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements an inverse battle challenge.
|
|
*/
|
|
export class InverseBattleChallenge extends Challenge {
|
|
public override get ribbonAwarded(): RibbonFlag {
|
|
return this.value ? RibbonData.INVERSE : 0n;
|
|
}
|
|
constructor() {
|
|
super(Challenges.INVERSE_BATTLE, 1);
|
|
}
|
|
|
|
static loadChallenge(source: InverseBattleChallenge | any): InverseBattleChallenge {
|
|
const newChallenge = new InverseBattleChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
|
|
override getDifficulty(): number {
|
|
return 0;
|
|
}
|
|
|
|
applyTypeEffectiveness(effectiveness: NumberHolder): boolean {
|
|
if (effectiveness.value < 1) {
|
|
effectiveness.value = 2;
|
|
return true;
|
|
}
|
|
if (effectiveness.value > 1) {
|
|
effectiveness.value = 0.5;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements a flip stat challenge.
|
|
*/
|
|
export class FlipStatChallenge extends Challenge {
|
|
public override get ribbonAwarded(): RibbonFlag {
|
|
return this.value ? RibbonData.FLIP_STATS : 0n;
|
|
}
|
|
constructor() {
|
|
super(Challenges.FLIP_STAT, 1);
|
|
}
|
|
|
|
override applyFlipStat(_pokemon: Pokemon, baseStats: number[]) {
|
|
const origStats = deepCopy(baseStats);
|
|
baseStats[0] = origStats[5];
|
|
baseStats[1] = origStats[4];
|
|
baseStats[2] = origStats[3];
|
|
baseStats[3] = origStats[2];
|
|
baseStats[4] = origStats[1];
|
|
baseStats[5] = origStats[0];
|
|
return true;
|
|
}
|
|
|
|
static loadChallenge(source: FlipStatChallenge | any): FlipStatChallenge {
|
|
const newChallenge = new FlipStatChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lowers the amount of starter points available.
|
|
*/
|
|
export class LowerStarterMaxCostChallenge extends Challenge {
|
|
constructor() {
|
|
super(Challenges.LOWER_MAX_STARTER_COST, 9);
|
|
}
|
|
|
|
getValue(overrideValue: number = this.value): string {
|
|
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
|
|
}
|
|
|
|
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
|
|
if (speciesStarterCosts[pokemon.speciesId] > DEFAULT_PARTY_MAX_COST - this.value) {
|
|
valid.value = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static loadChallenge(source: LowerStarterMaxCostChallenge | any): LowerStarterMaxCostChallenge {
|
|
const newChallenge = new LowerStarterMaxCostChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lowers the maximum cost of starters available.
|
|
*/
|
|
export class LowerStarterPointsChallenge extends Challenge {
|
|
constructor() {
|
|
super(Challenges.LOWER_STARTER_POINTS, 9);
|
|
}
|
|
|
|
getValue(overrideValue: number = this.value): string {
|
|
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
|
|
}
|
|
|
|
applyStarterPoints(points: NumberHolder): boolean {
|
|
points.value -= this.value;
|
|
return true;
|
|
}
|
|
|
|
static loadChallenge(source: LowerStarterPointsChallenge | any): LowerStarterPointsChallenge {
|
|
const newChallenge = new LowerStarterPointsChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements a No Support challenge
|
|
*/
|
|
export class LimitedSupportChallenge extends Challenge {
|
|
public override get ribbonAwarded(): RibbonFlag {
|
|
switch (this.value) {
|
|
case 1:
|
|
return RibbonData.NO_HEAL as RibbonFlag;
|
|
case 2:
|
|
return RibbonData.NO_SHOP as RibbonFlag;
|
|
case 3:
|
|
return (RibbonData.NO_HEAL | RibbonData.NO_SHOP | RibbonData.NO_SUPPORT) as RibbonFlag;
|
|
default:
|
|
return 0n as RibbonFlag;
|
|
}
|
|
}
|
|
constructor() {
|
|
super(Challenges.LIMITED_SUPPORT, 3);
|
|
}
|
|
|
|
override applyPartyHeal(status: BooleanHolder): boolean {
|
|
if (status.value) {
|
|
status.value = this.value === 2;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
override applyShop(status: BooleanHolder): boolean {
|
|
if (status.value) {
|
|
status.value = this.value === 1;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static override loadChallenge(source: LimitedSupportChallenge | any): LimitedSupportChallenge {
|
|
const newChallenge = new LimitedSupportChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements a Limited Catch challenge
|
|
*/
|
|
export class LimitedCatchChallenge extends Challenge {
|
|
public override get ribbonAwarded(): RibbonFlag {
|
|
return this.value ? RibbonData.LIMITED_CATCH : 0n;
|
|
}
|
|
constructor() {
|
|
super(Challenges.LIMITED_CATCH, 1);
|
|
}
|
|
|
|
override applyPokemonAddToParty(pokemon: EnemyPokemon, status: BooleanHolder): boolean {
|
|
if (status.value) {
|
|
const isTeleporter =
|
|
globalScene.currentBattle.mysteryEncounter?.encounterType === MysteryEncounterType.TELEPORTING_HIJINKS
|
|
&& globalScene.currentBattle.mysteryEncounter.selectedOption
|
|
!== globalScene.currentBattle.mysteryEncounter.options[2]; // don't allow catch when not choosing biome change option
|
|
const isFirstWave = pokemon.metWave % 10 === 1;
|
|
status.value = isTeleporter || isFirstWave;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static override loadChallenge(source: LimitedCatchChallenge | any): LimitedCatchChallenge {
|
|
const newChallenge = new LimitedCatchChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements a Permanent Faint challenge
|
|
*/
|
|
export class HardcoreChallenge extends Challenge {
|
|
public override get ribbonAwarded(): RibbonFlag {
|
|
return this.value ? RibbonData.HARDCORE : 0n;
|
|
}
|
|
constructor() {
|
|
super(Challenges.HARDCORE, 1);
|
|
}
|
|
|
|
override applyPokemonFusion(pokemon: PlayerPokemon, status: BooleanHolder): boolean {
|
|
if (!status.value) {
|
|
status.value = pokemon.isFainted();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
override applyShopItem(shopItem: ModifierTypeOption | null, status: BooleanHolder): boolean {
|
|
status.value = shopItem?.type.group !== "revive";
|
|
return true;
|
|
}
|
|
|
|
override applyWaveReward(reward: ModifierTypeOption | null, status: BooleanHolder): boolean {
|
|
return this.applyShopItem(reward, status);
|
|
}
|
|
|
|
override applyPokemonMove(moveId: MoveId, status: BooleanHolder) {
|
|
if (status.value) {
|
|
status.value = moveId !== MoveId.REVIVAL_BLESSING;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
override applyPreventRevive(status: BooleanHolder): boolean {
|
|
if (!status.value) {
|
|
status.value = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static override loadChallenge(source: HardcoreChallenge | any): HardcoreChallenge {
|
|
const newChallenge = new HardcoreChallenge();
|
|
newChallenge.value = source.value;
|
|
newChallenge.severity = source.severity;
|
|
return newChallenge;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param source A challenge to copy, or an object of a challenge's properties. Missing values are treated as defaults.
|
|
* @returns The challenge in question.
|
|
*/
|
|
export function copyChallenge(source: Challenge | any): Challenge {
|
|
switch (source.id) {
|
|
case Challenges.SINGLE_GENERATION:
|
|
return SingleGenerationChallenge.loadChallenge(source);
|
|
case Challenges.SINGLE_TYPE:
|
|
return SingleTypeChallenge.loadChallenge(source);
|
|
case Challenges.LOWER_MAX_STARTER_COST:
|
|
return LowerStarterMaxCostChallenge.loadChallenge(source);
|
|
case Challenges.LOWER_STARTER_POINTS:
|
|
return LowerStarterPointsChallenge.loadChallenge(source);
|
|
case Challenges.FRESH_START:
|
|
return FreshStartChallenge.loadChallenge(source);
|
|
case Challenges.INVERSE_BATTLE:
|
|
return InverseBattleChallenge.loadChallenge(source);
|
|
case Challenges.FLIP_STAT:
|
|
return FlipStatChallenge.loadChallenge(source);
|
|
case Challenges.LIMITED_CATCH:
|
|
return LimitedCatchChallenge.loadChallenge(source);
|
|
case Challenges.LIMITED_SUPPORT:
|
|
return LimitedSupportChallenge.loadChallenge(source);
|
|
case Challenges.HARDCORE:
|
|
return HardcoreChallenge.loadChallenge(source);
|
|
}
|
|
throw new Error("Unknown challenge copied");
|
|
}
|
|
|
|
export const allChallenges: Challenge[] = [];
|
|
|
|
export function initChallenges() {
|
|
allChallenges.push(
|
|
new FreshStartChallenge(),
|
|
new HardcoreChallenge(),
|
|
new LimitedCatchChallenge(),
|
|
new LimitedSupportChallenge(),
|
|
new SingleGenerationChallenge(),
|
|
new SingleTypeChallenge(),
|
|
new InverseBattleChallenge(),
|
|
new FlipStatChallenge(),
|
|
);
|
|
}
|