From 696ff6eae38a1b5b813ae95705d07dfeea336de6 Mon Sep 17 00:00:00 2001 From: Xavion3 Date: Sat, 8 Jun 2024 15:07:23 +1000 Subject: [PATCH] Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic --- src/battle-scene.ts | 10 +- src/battle.ts | 4 +- src/data/challenge.ts | 568 +++++++++++++++++++++++++ src/data/daily-run.ts | 3 +- src/data/enums/challenges.ts | 7 + src/evolution-phase.ts | 5 + src/field/pokemon.ts | 20 +- src/game-mode.ts | 86 +++- src/loading-scene.ts | 2 + src/locales/de/challenges.ts | 67 +++ src/locales/de/config.ts | 2 + src/locales/en/achv.ts | 95 +++++ src/locales/en/challenges.ts | 67 +++ src/locales/en/config.ts | 2 + src/locales/es/challenges.ts | 67 +++ src/locales/es/config.ts | 2 + src/locales/fr/challenges.ts | 67 +++ src/locales/fr/config.ts | 2 + src/locales/it/challenges.ts | 67 +++ src/locales/it/config.ts | 2 + src/locales/ko/challenges.ts | 67 +++ src/locales/ko/config.ts | 2 + src/locales/pt_BR/challenges.ts | 67 +++ src/locales/pt_BR/config.ts | 2 + src/locales/zh_CN/challenges.ts | 67 +++ src/locales/zh_CN/config.ts | 2 + src/locales/zh_TW/challenges.ts | 67 +++ src/locales/zh_TW/config.ts | 2 + src/phases.ts | 162 +++++-- src/plugins/i18n.ts | 1 + src/system/achv.ts | 71 ++++ src/system/challenge-data.ts | 17 + src/system/game-data.ts | 23 +- src/system/unlockables.ts | 6 +- src/test/battle/battle.test.ts | 4 +- src/test/utils/gameManager.ts | 5 +- src/test/utils/gameManagerUtils.ts | 4 +- src/ui/challenges-select-ui-handler.ts | 330 ++++++++++++++ src/ui/modifier-select-ui-handler.ts | 82 +++- src/ui/party-ui-handler.ts | 53 ++- src/ui/save-slot-select-ui-handler.ts | 4 +- src/ui/starter-select-ui-handler.ts | 49 ++- src/ui/ui-handler.ts | 7 + src/ui/ui.ts | 10 +- 44 files changed, 2115 insertions(+), 134 deletions(-) create mode 100644 src/data/challenge.ts create mode 100644 src/data/enums/challenges.ts create mode 100644 src/locales/de/challenges.ts create mode 100644 src/locales/en/challenges.ts create mode 100644 src/locales/es/challenges.ts create mode 100644 src/locales/fr/challenges.ts create mode 100644 src/locales/it/challenges.ts create mode 100644 src/locales/ko/challenges.ts create mode 100644 src/locales/pt_BR/challenges.ts create mode 100644 src/locales/zh_CN/challenges.ts create mode 100644 src/locales/zh_TW/challenges.ts create mode 100644 src/system/challenge-data.ts create mode 100644 src/ui/challenges-select-ui-handler.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 0a9c881fb4d..91dbea3c187 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -20,8 +20,8 @@ import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesF import AbilityBar from "./ui/ability-bar"; import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability"; import { allAbilities } from "./data/ability"; -import Battle, { BattleType, FixedBattleConfig, fixedBattles } from "./battle"; -import { GameMode, GameModes, gameModes } from "./game-mode"; +import Battle, { BattleType, FixedBattleConfig } from "./battle"; +import { GameMode, GameModes, getGameMode } from "./game-mode"; import FieldSpritePipeline from "./pipelines/field-sprite"; import SpritePipeline from "./pipelines/sprite"; import PartyExpBar from "./ui/party-exp-bar"; @@ -853,7 +853,7 @@ export default class BattleScene extends SceneBase { this.gameData = new GameData(this); } - this.gameMode = gameModes[GameModes.CLASSIC]; + this.gameMode = getGameMode(GameModes.CLASSIC); this.setSeed(Overrides.SEED_OVERRIDE || Utils.randomString(24)); console.log("Seed:", this.seed); @@ -961,8 +961,8 @@ export default class BattleScene extends SceneBase { const playerField = this.getPlayerField(); - if (this.gameMode.hasFixedBattles && fixedBattles.hasOwnProperty(newWaveIndex) && trainerData === undefined) { - battleConfig = fixedBattles[newWaveIndex]; + if (this.gameMode.isFixedBattle(newWaveIndex) && trainerData === undefined) { + battleConfig = this.gameMode.getFixedBattle(newWaveIndex); newDouble = battleConfig.double; newBattleType = battleConfig.battleType; this.executeWithSeedOffset(() => newTrainer = battleConfig.getTrainer(this), (battleConfig.seedOffsetWaveIndex || newWaveIndex) << 8); diff --git a/src/battle.ts b/src/battle.ts index 4f941eaa653..0f70d9a9cb5 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -416,11 +416,11 @@ function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[]): Get }; } -interface FixedBattleConfigs { +export interface FixedBattleConfigs { [key: integer]: FixedBattleConfig } -export const fixedBattles: FixedBattleConfigs = { +export const classicFixedBattles: FixedBattleConfigs = { [5]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.YOUNGSTER, Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT)), [8]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) diff --git a/src/data/challenge.ts b/src/data/challenge.ts new file mode 100644 index 00000000000..2aad5e75f2d --- /dev/null +++ b/src/data/challenge.ts @@ -0,0 +1,568 @@ +import * as Utils from "../utils"; +import { Challenges } from "./enums/challenges"; +import i18next from "#app/plugins/i18n.js"; +import { GameData } from "#app/system/game-data.js"; +import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species"; +import Pokemon from "#app/field/pokemon.js"; +import { BattleType, FixedBattleConfig } from "#app/battle.js"; +import { TrainerType } from "./enums/trainer-type"; +import Trainer, { TrainerVariant } from "#app/field/trainer.js"; +import { GameMode } from "#app/game-mode.js"; +import { Species } from "./enums/species"; +import { Type } from "./type"; + +/** + * An enum for all the challenge types. The parameter entries on these describe the + * parameters to use when calling the applyChallenges function. + */ +export enum ChallengeType { + /** + * Challenges which modify what starters you can choose + * @param args [0] {@link PokemonSpecies} The species to check + * [1] {@link Utils.BooleanHolder} Sets to false if illegal, pass in true. + */ + STARTER_CHOICE, + /** + * Challenges which modify how many starter points you have + * @param args [0] {@link Utils.NumberHolder} The amount of starter points you have + */ + STARTER_POINTS, + /** + * Challenges which modify your starters in some way + * Not Fully Implemented + */ + STARTER_MODIFY, + /** + * Challenges which limit which pokemon you can have in battle. + * @param args [0] {@link Pokemon} The pokemon to check + * [1] {@link Utils.BooleanHolder} Sets to false if illegal, pass in true. + */ + POKEMON_IN_BATTLE, + /** + * Adds or modifies the fixed battles in a run + * @param args [0] integer The wave to get a battle for + * [1] {@link FixedBattleConfig} A new fixed battle. It'll be modified if a battle exists. + */ + FIXED_BATTLES, +} + +/** + * 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. + + public conditions: ChallengeCondition[]; + public challengeTypes: ChallengeType[]; + + /** + * @param {Challenges} id The enum value for the challenge + */ + constructor(id: Challenges, maxValue: integer = Number.MAX_SAFE_INTEGER) { + this.id = id; + + this.value = 0; + this.maxValue = maxValue; + this.severity = 0; + this.maxSeverity = 0; + this.conditions = []; + this.challengeTypes = []; + } + + /** + * Reset the challenge to a base state. + */ + reset(): void { + this.value = 0; + this.severity = 0; + } + + /** + * Gets the localisation key for the 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(""); + } + + /** + * Used for unlockable challenges to check if they're unlocked. + * @param {GameData} data The save data. + * @returns {boolean} Whether this challenge is unlocked. + */ + isUnlocked(data: GameData): boolean { + return this.conditions.every(f => f(data)); + } + + /** + * Adds an unlock condition to this challenge. + * @param {ChallengeCondition} condition The condition to add. + * @returns {Challenge} This challenge + */ + condition(condition: ChallengeCondition): Challenge { + this.conditions.push(condition); + + return this; + } + + /** + * If this challenge is of a particular type + * @param {ChallengeType} challengeType The challenge type to check. + * @returns {Challenge} This challenge + */ + isOfType(challengeType: ChallengeType): boolean { + return this.challengeTypes.some(c => c === challengeType); + } + + /** + * Adds a challenge type to this challenge. + * @param {ChallengeType} challengeType The challenge type to add. + * @returns {Challenge} This challenge + */ + addChallengeType(challengeType: ChallengeType): Challenge { + this.challengeTypes.push(challengeType); + + return this; + } + + /** + * @returns {string} The localised name of this challenge. + */ + getName(): string { + return i18next.t(`challenges:${this.geti18nKey()}.name`); + } + + /** + * 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. + */ + getValue(overrideValue?: integer): string { + if (overrideValue === undefined) { + overrideValue = this.value; + } + return i18next.t(`challenges:${this.geti18nKey()}.value.${this.value}`); + } + + /** + * 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. + */ + getDescription(overrideValue?: integer): string { + if (overrideValue === undefined) { + overrideValue = this.value; + } + return i18next.t(`challenges:${this.geti18nKey()}.desc.${this.value}`); + } + + /** + * Increase the value of the challenge + * @returns {boolean} 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 {boolean} 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 {boolean} 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 {boolean} 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 {integer} The difficulty value. + */ + getDifficulty(): integer { + return this.value; + } + + /** + * Gets the minimum difficulty added by this challenge. + * @returns {integer} The difficulty value. + */ + getMinDifficulty(): integer { + return 0; + } + + /** + * Modifies the data or game state in some way to apply the challenge. + * @param {ChallengeType} challengeType Which challenge type this is being applied for. + * @param args Irrelevant. See the specific challenge's apply function for additional information. + */ + abstract apply(challengeType: ChallengeType, args: any[]): boolean; + + /** + * Clones a challenge, either from another challenge or json. Chainable. + * @param {Challenge | any} source The source challenge of json. + * @returns {Challenge} This challenge. + */ + static loadChallenge(source: Challenge | any): Challenge { + throw new Error("Method not implemented! Use derived class"); + } +} + +type ChallengeCondition = (data: GameData) => boolean; + +/** + * Implements a mono generation challenge. + */ +export class SingleGenerationChallenge extends Challenge { + constructor() { + super(Challenges.SINGLE_GENERATION, 9); + this.addChallengeType(ChallengeType.STARTER_CHOICE); + this.addChallengeType(ChallengeType.POKEMON_IN_BATTLE); + this.addChallengeType(ChallengeType.FIXED_BATTLES); + } + + apply(challengeType: ChallengeType, args: any[]): boolean { + if (this.value === 0) { + return false; + } + + switch (challengeType) { + case ChallengeType.STARTER_CHOICE: + const species = args[0] as PokemonSpecies; + const isValidStarter = args[1] as Utils.BooleanHolder; + if (species.generation !== this.value) { + isValidStarter.value = false; + return true; + } + break; + case ChallengeType.POKEMON_IN_BATTLE: + const pokemon = args[0] as Pokemon; + const isValidPokemon = args[1] as Utils.BooleanHolder; + if (pokemon.isPlayer() && ((pokemon.species.formIndex === 0 ? pokemon.species : getPokemonSpecies(pokemon.species.speciesId)).generation !== this.value || (pokemon.isFusion() && (pokemon.fusionSpecies.formIndex === 0 ? pokemon.fusionSpecies : getPokemonSpecies(pokemon.fusionSpecies.speciesId)).generation !== this.value))) { + isValidPokemon.value = false; + return true; + } + break; + case ChallengeType.FIXED_BATTLES: + const waveIndex = args[0] as integer; + const battleConfig = args[1] as FixedBattleConfig; + 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 ]; + 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 ]; + 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 ]) ]; + break; + } + if (trainerTypes.length === 0) { + return false; + } else { + battleConfig.setBattleType(BattleType.TRAINER).setGetTrainerFunc(scene => new Trainer(scene, trainerTypes[this.value - 1], TrainerVariant.DEFAULT)); + return true; + } + } + return false; + } + + /** + * @overrides + */ + getDifficulty(): number { + return this.value > 0 ? 1 : 0; + } + + 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: Species; + /** The type to count as */ + type: Type; + /** 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 { + private static TYPE_OVERRIDES: monotypeOverride[] = [ + {species: Species.MELOETTA, type: Type.PSYCHIC, fusion: true}, + {species: Species.CASTFORM, type: Type.NORMAL, fusion: false}, + ]; + + constructor() { + super(Challenges.SINGLE_TYPE, 18); + this.addChallengeType(ChallengeType.STARTER_CHOICE); + this.addChallengeType(ChallengeType.POKEMON_IN_BATTLE); + } + + apply(challengeType: ChallengeType, args: any[]): boolean { + if (this.value === 0) { + return false; + } + + switch (challengeType) { + case ChallengeType.STARTER_CHOICE: + const species = args[0] as PokemonSpecies; + const isValidStarter = args[1] as Utils.BooleanHolder; + if (!species.isOfType(this.value - 1)) { + isValidStarter.value = false; + return true; + } + break; + case ChallengeType.POKEMON_IN_BATTLE: + const pokemon = args[0] as Pokemon; + const isValidPokemon = args[1] as Utils.BooleanHolder; + 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)) { + isValidPokemon.value = false; + return true; + } + break; + } + return false; + } + + /** + * @overrides + */ + getDifficulty(): number { + return this.value > 0 ? 1 : 0; + } + + 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 { + constructor() { + super(Challenges.FRESH_START, 1); + this.addChallengeType(ChallengeType.STARTER_CHOICE); + this.addChallengeType(ChallengeType.STARTER_MODIFY); + } + + apply(challengeType: ChallengeType, args: any[]): boolean { + if (this.value === 0) { + return false; + } + + switch (challengeType) { + case ChallengeType.STARTER_CHOICE: + const species = args[0] as PokemonSpecies; + const isValidStarter = args[1] as Utils.BooleanHolder; + if (species) { + isValidStarter.value = false; + return true; + } + break; + } + return false; + } + + /** + * @overrides + */ + getDifficulty(): number { + return 0; + } + + static loadChallenge(source: FreshStartChallenge | any): FreshStartChallenge { + const newChallenge = new FreshStartChallenge(); + 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); + this.addChallengeType(ChallengeType.STARTER_CHOICE); + } + + /** + * @override + */ + getValue(overrideValue?: integer): string { + if (overrideValue === undefined) { + overrideValue = this.value; + } + return (10 - overrideValue).toString(); + } + + apply(challengeType: ChallengeType, args: any[]): boolean { + if (this.value === 0) { + return false; + } + + switch (challengeType) { + case ChallengeType.STARTER_CHOICE: + const species = args[0] as PokemonSpecies; + const isValid = args[1] as Utils.BooleanHolder; + if (speciesStarters[species.speciesId] > 10 - this.value) { + isValid.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); + this.addChallengeType(ChallengeType.STARTER_POINTS); + } + + /** + * @override + */ + getValue(overrideValue?: integer): string { + if (overrideValue === undefined) { + overrideValue = this.value; + } + return (10 - overrideValue).toString(); + } + + apply(challengeType: ChallengeType, args: any[]): boolean { + if (this.value === 0) { + return false; + } + + switch (challengeType) { + case ChallengeType.STARTER_POINTS: + const points = args[0] as Utils.NumberHolder; + points.value -= this.value; + return true; + } + return false; + } + + static loadChallenge(source: LowerStarterPointsChallenge | any): LowerStarterPointsChallenge { + const newChallenge = new LowerStarterPointsChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + +/** + * Apply all challenges of a given challenge type. + * @param {BattleScene} scene The current scene + * @param {ChallengeType} challengeType What challenge type to apply + * @param {any[]} args Any args for that challenge type + * @returns {boolean} True if any challenge was successfully applied. + */ +export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType, ...args: any[]): boolean { + let ret = false; + gameMode.challenges.forEach(v => { + if (v.isOfType(challengeType)) { + ret ||= v.apply(challengeType, args); + } + }); + return ret; +} + +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); + } + throw new Error("Unknown challenge copied"); +} + +export const allChallenges: Challenge[] = []; + +export function initChallenges() { + allChallenges.push( + new SingleGenerationChallenge(), + new SingleTypeChallenge(), + // new LowerStarterMaxCostChallenge(), + // new LowerStarterPointsChallenge(), + // new FreshStartChallenge() + ); +} diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index cb4bfddc685..c9b097bfcc0 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -1,6 +1,5 @@ import BattleScene from "../battle-scene"; import { PlayerPokemon } from "../field/pokemon"; -import { GameModes, gameModes } from "../game-mode"; import { Starter } from "../ui/starter-select-ui-handler"; import * as Utils from "../utils"; import { Species } from "./enums/species"; @@ -29,7 +28,7 @@ export function getDailyRunStarters(scene: BattleScene, seed: string): Starter[] const starters: Starter[] = []; scene.executeWithSeedOffset(() => { - const startingLevel = gameModes[GameModes.DAILY].getStartingLevel(); + const startingLevel = scene.gameMode.getStartingLevel(); if (/\d{18}$/.test(seed)) { for (let s = 0; s < 3; s++) { diff --git a/src/data/enums/challenges.ts b/src/data/enums/challenges.ts new file mode 100644 index 00000000000..690e1cdc32d --- /dev/null +++ b/src/data/enums/challenges.ts @@ -0,0 +1,7 @@ +export enum Challenges { + SINGLE_GENERATION, + SINGLE_TYPE, + LOWER_MAX_STARTER_COST, + LOWER_STARTER_POINTS, + FRESH_START +} diff --git a/src/evolution-phase.ts b/src/evolution-phase.ts index 29382807ccb..c7986f6664f 100644 --- a/src/evolution-phase.ts +++ b/src/evolution-phase.ts @@ -531,6 +531,11 @@ export class EvolutionPhase extends Phase { } export class EndEvolutionPhase extends Phase { + + constructor(scene: BattleScene) { + super(scene); + } + start() { super.start(); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ff277fc865b..8a3d21fd390 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -48,6 +48,7 @@ import { BerryType } from "../data/enums/berry-type"; import i18next from "../plugins/i18n"; import { speciesEggMoves } from "../data/egg-moves"; import { ModifierTier } from "../modifier/modifier-tier"; +import { applyChallenges, ChallengeType } from "#app/data/challenge.js"; export enum FieldPosition { CENTER, @@ -266,11 +267,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT); } + /** + * Check if this pokemon is both not fainted and allowed to be in battle. + * This is frequently a better alternative to {@link isFainted} + * @returns {boolean} True if pokemon is allowed in battle + */ + isAllowedInBattle(): boolean { + const challengeAllowed = new Utils.BooleanHolder(true); + applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed); + return !this.isFainted() && challengeAllowed.value; + } + isActive(onField?: boolean): boolean { if (!this.scene) { return false; } - return !this.isFainted() && !!this.scene && (!onField || this.isOnField()); + return this.isAllowedInBattle() && !!this.scene && (!onField || this.isOnField()); } getDexAttr(): bigint { @@ -885,8 +897,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return types; } - isOfType(type: Type, forDefend: boolean = false): boolean { - return !!this.getTypes(true, forDefend).find(t => t === type); + isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride?: boolean): boolean { + return !!this.getTypes(includeTeraType, forDefend, ignoreOverride).some(t => t === type); } /** @@ -1054,7 +1066,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } isGrounded(): boolean { - return !this.isOfType(Type.FLYING, true) && !this.hasAbility(Abilities.LEVITATE); + return !this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE); } getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove): TypeDamageMultiplier { diff --git a/src/game-mode.ts b/src/game-mode.ts index 4d46971dcb5..258d3b74980 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -1,4 +1,4 @@ -import { fixedBattles } from "./battle"; +import { classicFixedBattles, FixedBattleConfig, FixedBattleConfigs } from "./battle"; import BattleScene from "./battle-scene"; import { Biome } from "./data/enums/biome"; import { Species } from "./data/enums/species"; @@ -6,12 +6,14 @@ import PokemonSpecies, { allSpecies } from "./data/pokemon-species"; import { Arena } from "./field/arena"; import * as Utils from "./utils"; import * as Overrides from "./overrides"; +import { allChallenges, applyChallenges, Challenge, ChallengeType, copyChallenge } from "./data/challenge"; export enum GameModes { CLASSIC, ENDLESS, SPLICED_ENDLESS, - DAILY + DAILY, + CHALLENGE } interface GameModeConfig { @@ -19,12 +21,12 @@ interface GameModeConfig { isEndless?: boolean; isDaily?: boolean; hasTrainers?: boolean; - hasFixedBattles?: boolean; hasNoShop?: boolean; hasShortBiomes?: boolean; hasRandomBiomes?: boolean; hasRandomBosses?: boolean; isSplicedOnly?: boolean; + isChallenge?: boolean; } export class GameMode implements GameModeConfig { @@ -33,16 +35,23 @@ export class GameMode implements GameModeConfig { public isEndless: boolean; public isDaily: boolean; public hasTrainers: boolean; - public hasFixedBattles: boolean; public hasNoShop: boolean; public hasShortBiomes: boolean; public hasRandomBiomes: boolean; public hasRandomBosses: boolean; public isSplicedOnly: boolean; + public isChallenge: boolean; + public challenges: Challenge[]; + public battleConfig: FixedBattleConfigs; - constructor(modeId: GameModes, config: GameModeConfig) { + constructor(modeId: GameModes, config: GameModeConfig, battleConfig?: FixedBattleConfigs) { this.modeId = modeId; + this.challenges = []; Object.assign(this, config); + if (this.isChallenge) { + this.challenges = allChallenges.map(c => copyChallenge(c)); + } + this.battleConfig = battleConfig || {}; } /** @@ -112,7 +121,7 @@ export class GameMode implements GameModeConfig { if (w === waveIndex) { continue; } - if ((w % 30) === (arena.scene.offsetGym ? 0 : 20) || fixedBattles.hasOwnProperty(w)) { + if ((w % 30) === (arena.scene.offsetGym ? 0 : 20) || this.isFixedBattle(waveIndex)) { allowTrainerBattle = false; break; } else if (w < waveIndex) { @@ -161,6 +170,7 @@ export class GameMode implements GameModeConfig { isWaveFinal(waveIndex: integer, modeId: GameModes = this.modeId): boolean { switch (modeId) { case GameModes.CLASSIC: + case GameModes.CHALLENGE: return waveIndex === 200; case GameModes.ENDLESS: case GameModes.SPLICED_ENDLESS: @@ -208,10 +218,36 @@ export class GameMode implements GameModeConfig { (this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS); } + /** + * Checks whether there is a fixed battle on this gamemode on a given wave. + * @param {integer} waveIndex The wave to check. + * @returns {boolean} If this game mode has a fixed battle on this wave + */ + isFixedBattle(waveIndex: integer): boolean { + const dummyConfig = new FixedBattleConfig(); + return this.battleConfig.hasOwnProperty(waveIndex) || applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, dummyConfig); + + } + + /** + * Returns the config for the fixed battle for a particular wave. + * @param {integer} waveIndex The wave to check. + * @returns {boolean} The fixed battle for this wave. + */ + getFixedBattle(waveIndex: integer): FixedBattleConfig { + const challengeConfig = new FixedBattleConfig(); + if (applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, challengeConfig)) { + return challengeConfig; + } else { + return this.battleConfig[waveIndex]; + } + } + getClearScoreBonus(): integer { switch (this.modeId) { case GameModes.CLASSIC: + case GameModes.CHALLENGE: return 5000; case GameModes.DAILY: return 2500; @@ -221,6 +257,7 @@ export class GameMode implements GameModeConfig { getEnemyModifierChance(isBoss: boolean): integer { switch (this.modeId) { case GameModes.CLASSIC: + case GameModes.CHALLENGE: case GameModes.DAILY: return !isBoss ? 18 : 6; case GameModes.ENDLESS: @@ -239,13 +276,38 @@ export class GameMode implements GameModeConfig { return "Endless (Spliced)"; case GameModes.DAILY: return "Daily Run"; + case GameModes.CHALLENGE: + return "Challenge"; + } + } + + static getModeName(modeId: GameModes): string { + switch (modeId) { + case GameModes.CLASSIC: + return "Classic"; + case GameModes.ENDLESS: + return "Endless"; + case GameModes.SPLICED_ENDLESS: + return "Endless (Spliced)"; + case GameModes.DAILY: + return "Daily Run"; + case GameModes.CHALLENGE: + return "Challenge"; } } } -export const gameModes = Object.freeze({ - [GameModes.CLASSIC]: new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true, hasFixedBattles: true }), - [GameModes.ENDLESS]: new GameMode(GameModes.ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true }), - [GameModes.SPLICED_ENDLESS]: new GameMode(GameModes.SPLICED_ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true, isSplicedOnly: true }), - [GameModes.DAILY]: new GameMode(GameModes.DAILY, { isDaily: true, hasTrainers: true, hasNoShop: true }) -}); +export function getGameMode(gameMode: GameModes): GameMode { + switch (gameMode) { + case GameModes.CLASSIC: + return new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true }, classicFixedBattles); + case GameModes.ENDLESS: + return new GameMode(GameModes.ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true }); + case GameModes.SPLICED_ENDLESS: + return new GameMode(GameModes.SPLICED_ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true, isSplicedOnly: true }); + case GameModes.DAILY: + return new GameMode(GameModes.DAILY, { isDaily: true, hasTrainers: true, hasNoShop: true }); + case GameModes.CHALLENGE: + return new GameMode(GameModes.CHALLENGE, { isClassic: true, hasTrainers: true, isChallenge: true }, classicFixedBattles); + } +} diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 22c013d9481..341dc7abb51 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -18,6 +18,7 @@ import {initMoves} from "#app/data/move"; import {initAbilities} from "#app/data/ability"; import {initAchievements} from "#app/system/achv"; import {initTrainerTypeDialogue} from "#app/data/dialogue"; +import { initChallenges } from "./data/challenge"; import i18next from "i18next"; import { initStatsKeys } from "./ui/game-stats-ui-handler"; import { initVouchers } from "./system/voucher"; @@ -341,6 +342,7 @@ export class LoadingScene extends SceneBase { initSpecies(); initMoves(); initAbilities(); + initChallenges(); } loadLoadingScreen() { diff --git a/src/locales/de/challenges.ts b/src/locales/de/challenges.ts new file mode 100644 index 00000000000..149037be740 --- /dev/null +++ b/src/locales/de/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "points": "Bad Ideas", + "confirm_start": "Proceed with these challenges?", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/de/config.ts b/src/locales/de/config.ts index afee437a652..c9fd238b986 100644 --- a/src/locales/de/config.ts +++ b/src/locales/de/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const deConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/locales/en/achv.ts b/src/locales/en/achv.ts index 42b1995bcde..a545ab8a71e 100644 --- a/src/locales/en/achv.ts +++ b/src/locales/en/achv.ts @@ -168,4 +168,99 @@ export const achv: AchievementTranslationEntries = { name: "Undefeated", description: "Beat the game in classic mode", }, + + "MONO_GEN_ONE": { + name: "The Original Rival", + description: "Complete the generation one only challenge.", + }, + "MONO_GEN_TWO": { + name: "Generation 1.5", + description: "Complete the generation two only challenge.", + }, + "MONO_GEN_THREE": { + name: "Too much water?", + description: "Complete the generation three only challenge.", + }, + "MONO_GEN_FOUR": { + name: "Is she really the hardest?", + description: "Complete the generation four only challenge.", + }, + "MONO_GEN_FIVE": { + name: "All Original", + description: "Complete the generation five only challenge.", + }, + "MONO_GEN_SIX": { + name: "Almost Royalty", + description: "Complete the generation six only challenge.", + }, + "MONO_GEN_SEVEN": { + name: "Only Technically", + description: "Complete the generation seven only challenge.", + }, + "MONO_GEN_EIGHT": { + name: "A Champion Time!", + description: "Complete the generation eight only challenge.", + }, + "MONO_GEN_NINE": { + name: "She was going easy on you", + description: "Complete the generation nine only challenge.", + }, + + "MonoType": { + description: "Complete the {{type}} monotype challenge.", + }, + "MONO_NORMAL": { + name: "Mono NORMAL", + }, + "MONO_FIGHTING": { + name: "I Know Kung Fu", + }, + "MONO_FLYING": { + name: "Mono FLYING", + }, + "MONO_POISON": { + name: "Kanto's Favourite", + }, + "MONO_GROUND": { + name: "Mono GROUND", + }, + "MONO_ROCK": { + name: "Brock Hard", + }, + "MONO_BUG": { + name: "Sting Like A Beedrill", + }, + "MONO_GHOST": { + name: "Who you gonna call?", + }, + "MONO_STEEL": { + name: "Mono STEEL", + }, + "MONO_FIRE": { + name: "Mono FIRE", + }, + "MONO_WATER": { + name: "When It Rains, It Pours", + }, + "MONO_GRASS": { + name: "Mono GRASS", + }, + "MONO_ELECTRIC": { + name: "Mono ELECTRIC", + }, + "MONO_PSYCHIC": { + name: "Mono PSYCHIC", + }, + "MONO_ICE": { + name: "Mono ICE", + }, + "MONO_DRAGON": { + name: "Mono DRAGON", + }, + "MONO_DARK": { + name: "It's just a phase", + }, + "MONO_FAIRY": { + name: "Mono FAIRY", + }, } as const; diff --git a/src/locales/en/challenges.ts b/src/locales/en/challenges.ts new file mode 100644 index 00000000000..7401104e1a3 --- /dev/null +++ b/src/locales/en/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "start": "Start", + "illegalEvolution": "{{pokemon}} changed into an ineligble pokemon\nfor this challenge!", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index 383b52d4c19..b3d3417c439 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const enConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/locales/es/challenges.ts b/src/locales/es/challenges.ts new file mode 100644 index 00000000000..149037be740 --- /dev/null +++ b/src/locales/es/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "points": "Bad Ideas", + "confirm_start": "Proceed with these challenges?", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/es/config.ts b/src/locales/es/config.ts index f6d1ac0f1c1..7eb3b2f6710 100644 --- a/src/locales/es/config.ts +++ b/src/locales/es/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const esConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/locales/fr/challenges.ts b/src/locales/fr/challenges.ts new file mode 100644 index 00000000000..149037be740 --- /dev/null +++ b/src/locales/fr/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "points": "Bad Ideas", + "confirm_start": "Proceed with these challenges?", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/fr/config.ts b/src/locales/fr/config.ts index d523d35bb87..9a0f7e5664e 100644 --- a/src/locales/fr/config.ts +++ b/src/locales/fr/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const frConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/locales/it/challenges.ts b/src/locales/it/challenges.ts new file mode 100644 index 00000000000..149037be740 --- /dev/null +++ b/src/locales/it/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "points": "Bad Ideas", + "confirm_start": "Proceed with these challenges?", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/it/config.ts b/src/locales/it/config.ts index 3f53c8fca01..1eb9b78177c 100644 --- a/src/locales/it/config.ts +++ b/src/locales/it/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const itConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/locales/ko/challenges.ts b/src/locales/ko/challenges.ts new file mode 100644 index 00000000000..149037be740 --- /dev/null +++ b/src/locales/ko/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "points": "Bad Ideas", + "confirm_start": "Proceed with these challenges?", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/ko/config.ts b/src/locales/ko/config.ts index 936154153be..90381c42fbe 100644 --- a/src/locales/ko/config.ts +++ b/src/locales/ko/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const koConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/locales/pt_BR/challenges.ts b/src/locales/pt_BR/challenges.ts new file mode 100644 index 00000000000..149037be740 --- /dev/null +++ b/src/locales/pt_BR/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "points": "Bad Ideas", + "confirm_start": "Proceed with these challenges?", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/pt_BR/config.ts b/src/locales/pt_BR/config.ts index 3baca7df382..64ceea7806d 100644 --- a/src/locales/pt_BR/config.ts +++ b/src/locales/pt_BR/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const ptBrConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/locales/zh_CN/challenges.ts b/src/locales/zh_CN/challenges.ts new file mode 100644 index 00000000000..149037be740 --- /dev/null +++ b/src/locales/zh_CN/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "points": "Bad Ideas", + "confirm_start": "Proceed with these challenges?", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/zh_CN/config.ts b/src/locales/zh_CN/config.ts index 0560b829dea..a35788bda49 100644 --- a/src/locales/zh_CN/config.ts +++ b/src/locales/zh_CN/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const zhCnConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/locales/zh_TW/challenges.ts b/src/locales/zh_TW/challenges.ts new file mode 100644 index 00000000000..149037be740 --- /dev/null +++ b/src/locales/zh_TW/challenges.ts @@ -0,0 +1,67 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const challenges: SimpleTranslationEntries = { + "title": "Challenge Modifiers", + "points": "Bad Ideas", + "confirm_start": "Proceed with these challenges?", + "singleGeneration.name": "Mono Gen", + "singleGeneration.value.0": "Off", + "singleGeneration.desc.0": "You can only use pokemon from the chosen generation.", + "singleGeneration.value.1": "Gen 1", + "singleGeneration.desc.1": "You can only use pokemon from generation one.", + "singleGeneration.value.2": "Gen 2", + "singleGeneration.desc.2": "You can only use pokemon from generation two.", + "singleGeneration.value.3": "Gen 3", + "singleGeneration.desc.3": "You can only use pokemon from generation three.", + "singleGeneration.value.4": "Gen 4", + "singleGeneration.desc.4": "You can only use pokemon from generation four.", + "singleGeneration.value.5": "Gen 5", + "singleGeneration.desc.5": "You can only use pokemon from generation five.", + "singleGeneration.value.6": "Gen 6", + "singleGeneration.desc.6": "You can only use pokemon from generation six.", + "singleGeneration.value.7": "Gen 7", + "singleGeneration.desc.7": "You can only use pokemon from generation seven.", + "singleGeneration.value.8": "Gen 8", + "singleGeneration.desc.8": "You can only use pokemon from generation eight.", + "singleGeneration.value.9": "Gen 9", + "singleGeneration.desc.9": "You can only use pokemon from generation nine.", + "singleType.name": "Mono Type", + "singleType.value.0": "Off", + "singleType.desc.0": "You can only use pokemon of the chosen type.", + "singleType.value.1": "Normal", + "singleType.desc.1": "You can only use pokemon with the Normal type.", + "singleType.value.2": "Fighting", + "singleType.desc.2": "You can only use pokemon with the Fighting type.", + "singleType.value.3": "Flying", + "singleType.desc.3": "You can only use pokemon with the Flying type.", + "singleType.value.4": "Poison", + "singleType.desc.4": "You can only use pokemon with the Poison type.", + "singleType.value.5": "Ground", + "singleType.desc.5": "You can only use pokemon with the Ground type.", + "singleType.value.6": "Rock", + "singleType.desc.6": "You can only use pokemon with the Rock type.", + "singleType.value.7": "Bug", + "singleType.desc.7": "You can only use pokemon with the Bug type.", + "singleType.value.8": "Ghost", + "singleType.desc.8": "You can only use pokemon with the Ghost type.", + "singleType.value.9": "Steel", + "singleType.desc.9": "You can only use pokemon with the Steel type.", + "singleType.value.10": "Fire", + "singleType.desc.10": "You can only use pokemon with the Fire type.", + "singleType.value.11": "Water", + "singleType.desc.11": "You can only use pokemon with the Water type.", + "singleType.value.12": "Grass", + "singleType.desc.12": "You can only use pokemon with the Grass type.", + "singleType.value.13": "Electric", + "singleType.desc.13": "You can only use pokemon with the Electric type.", + "singleType.value.14": "Psychic", + "singleType.desc.14": "You can only use pokemon with the Psychic type.", + "singleType.value.15": "Ice", + "singleType.desc.15": "You can only use pokemon with the Ice type.", + "singleType.value.16": "Dragon", + "singleType.desc.16": "You can only use pokemon with the Dragon type.", + "singleType.value.17": "Dark", + "singleType.desc.17": "You can only use pokemon with the Dark type.", + "singleType.value.18": "Fairy", + "singleType.desc.18": "You can only use pokemon with the Fairy type.", +} as const; diff --git a/src/locales/zh_TW/config.ts b/src/locales/zh_TW/config.ts index c7045da3d97..50ff5ed0db4 100644 --- a/src/locales/zh_TW/config.ts +++ b/src/locales/zh_TW/config.ts @@ -5,6 +5,7 @@ import { battle } from "./battle"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; import { berry } from "./berry"; import { biome } from "./biome"; +import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; import { PGFbattleSpecDialogue, @@ -45,6 +46,7 @@ export const zhTwConfig = { battleMessageUiHandler: battleMessageUiHandler, berry: berry, biome: biome, + challenges: challenges, commandUiHandler: commandUiHandler, PGMdialogue: PGMdialogue, PGFdialogue: PGFdialogue, diff --git a/src/phases.ts b/src/phases.ts index a235349cd6f..f83f90a626b 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -36,7 +36,7 @@ import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; import { BattleSpec } from "./enums/battle-spec"; import { Species } from "./data/enums/species"; -import { HealAchv, LevelAchv, achvs } from "./system/achv"; +import { ChallengeAchv, HealAchv, LevelAchv, achvs } from "./system/achv"; import { TrainerSlot, trainerConfigs } from "./data/trainer-config"; import { TrainerType } from "./data/enums/trainer-type"; import { EggHatchPhase } from "./egg-hatch-phase"; @@ -55,7 +55,7 @@ import { TerrainType } from "./data/terrain"; import { OptionSelectConfig, OptionSelectItem } from "./ui/abstact-option-select-ui-handler"; import { SaveSlotUiMode } from "./ui/save-slot-select-ui-handler"; import { fetchDailyRunSeed, getDailyRunStarters } from "./data/daily-run"; -import { GameModes, gameModes } from "./game-mode"; +import { GameMode, GameModes, getGameMode } from "./game-mode"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./data/pokemon-species"; import i18next from "./plugins/i18n"; import { Abilities } from "./data/enums/abilities"; @@ -202,14 +202,21 @@ export class TitlePhase extends Phase { if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { const options: OptionSelectItem[] = [ { - label: gameModes[GameModes.CLASSIC].getName(), + label: GameMode.getModeName(GameModes.CLASSIC), handler: () => { setModeAndEnd(GameModes.CLASSIC); return true; } }, { - label: gameModes[GameModes.ENDLESS].getName(), + label: GameMode.getModeName(GameModes.CHALLENGE), + handler: () => { + setModeAndEnd(GameModes.CHALLENGE); + return true; + } + }, + { + label: GameMode.getModeName(GameModes.ENDLESS), handler: () => { setModeAndEnd(GameModes.ENDLESS); return true; @@ -218,7 +225,7 @@ export class TitlePhase extends Phase { ]; if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { options.push({ - label: gameModes[GameModes.SPLICED_ENDLESS].getName(), + label: GameMode.getModeName(GameModes.SPLICED_ENDLESS), handler: () => { setModeAndEnd(GameModes.SPLICED_ENDLESS); return true; @@ -307,7 +314,7 @@ export class TitlePhase extends Phase { this.scene.sessionSlotId = slotId; const generateDaily = (seed: string) => { - this.scene.gameMode = gameModes[GameModes.DAILY]; + this.scene.gameMode = getGameMode(GameModes.DAILY); this.scene.setSeed(seed); this.scene.resetSeed(1); @@ -369,7 +376,12 @@ export class TitlePhase extends Phase { end(): void { if (!this.loaded && !this.scene.gameMode.isDaily) { this.scene.arena.preloadBgm(); - this.scene.pushPhase(new SelectStarterPhase(this.scene, this.gameMode)); + this.scene.gameMode = getGameMode(this.gameMode); + if (this.gameMode === GameModes.CHALLENGE) { + this.scene.pushPhase(new SelectChallengePhase(this.scene)); + } else { + this.scene.pushPhase(new SelectStarterPhase(this.scene)); + } this.scene.newArena(this.scene.gameMode.getStartingBiome(this.scene)); } else { this.scene.playBgm(); @@ -378,7 +390,7 @@ export class TitlePhase extends Phase { this.scene.pushPhase(new EncounterPhase(this.scene, this.loaded)); if (this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()).length; + const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; this.scene.pushPhase(new SummonPhase(this.scene, 0, true, true)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { @@ -504,13 +516,24 @@ export class SelectGenderPhase extends Phase { } } -export class SelectStarterPhase extends Phase { - private gameMode: GameModes; - - constructor(scene: BattleScene, gameMode: GameModes) { +export class SelectChallengePhase extends Phase { + constructor(scene: BattleScene) { super(scene); + } - this.gameMode = gameMode; + start() { + super.start(); + + this.scene.playBgm("menu"); + + this.scene.ui.setMode(Mode.CHALLENGE_SELECT); + } +} + +export class SelectStarterPhase extends Phase { + + constructor(scene: BattleScene) { + super(scene); } start() { @@ -529,7 +552,7 @@ export class SelectStarterPhase extends Phase { this.scene.sessionSlotId = slotId; this.initBattle(starters); }); - }, this.gameMode); + }); } initBattle(starters: Starter[]) { @@ -1002,7 +1025,7 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()); + const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); if (!availablePartyMembers[0].isOnField()) { this.scene.pushPhase(new SummonPhase(this.scene, 0)); @@ -1295,20 +1318,29 @@ export class SummonPhase extends PartyMemberPokemonPhase { */ preSummon(): void { const partyMember = this.getPokemon(); - // If the Pokemon about to be sent out is fainted, switch to the first non-fainted Pokemon - if (partyMember.isFainted()) { - console.warn("The Pokemon about to be sent out is fainted. Attempting to resolve..."); + // If the Pokemon about to be sent out is fainted or illegal under a challenge, switch to the first non-fainted legal Pokemon + if (!partyMember.isAllowedInBattle()) { + console.warn("The Pokemon about to be sent out is fainted or illegal under a challenge. Attempting to resolve..."); + + // First check if they're somehow still in play, if so remove them. + if (partyMember.isOnField()) { + partyMember.hideInfo(); + partyMember.setVisible(false); + this.scene.field.remove(partyMember); + this.scene.triggerPokemonFormChange(partyMember, SpeciesFormChangeActiveTrigger, true); + } + const party = this.getParty(); // Find the first non-fainted Pokemon index above the current one - const nonFaintedIndex = party.findIndex((p, i) => i > this.partyMemberIndex && !p.isFainted()); - if (nonFaintedIndex === -1) { + const legalIndex = party.findIndex((p, i) => i > this.partyMemberIndex && p.isAllowedInBattle()); + if (legalIndex === -1) { console.error("Party Details:\n", party); - throw new Error("All available Pokemon were fainted!"); + throw new Error("All available Pokemon were fainted or illegal!"); } - // Swaps the fainted Pokemon and the first non-fainted Pokemon in the party - [party[this.partyMemberIndex], party[nonFaintedIndex]] = [party[nonFaintedIndex], party[this.partyMemberIndex]]; + // Swaps the fainted Pokemon and the first non-fainted legal Pokemon in the party + [party[this.partyMemberIndex], party[legalIndex]] = [party[legalIndex], party[this.partyMemberIndex]]; console.warn("Swapped %s %O with %s %O", partyMember?.name, partyMember, party[0]?.name, party[0]); } @@ -1352,7 +1384,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { if (this.fieldIndex === 1) { pokemon.setFieldPosition(FieldPosition.RIGHT, 0); } else { - const availablePartyMembers = this.getParty().filter(p => !p.isFainted()).length; + const availablePartyMembers = this.getParty().filter(p => p.isAllowedInBattle()).length; pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT); } @@ -1642,7 +1674,7 @@ export class ToggleDoublePositionPhase extends BattlePhase { const playerPokemon = this.scene.getPlayerField().find(p => p.isActive(true)); if (playerPokemon) { - playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => !p.isFainted()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { + playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { if (playerPokemon.getFieldIndex() === 1) { const party = this.scene.getParty(); party[1] = party[0]; @@ -1742,6 +1774,34 @@ export class TurnInitPhase extends FieldPhase { start() { super.start(); + this.scene.getPlayerField().forEach(p => { + // If this pokemon is in play and evolved into something illegal under the current challenge, force a switch + if (p.isOnField() && !p.isAllowedInBattle()) { + this.scene.queueMessage(i18next.t("challenges:illegalEvolution", {"pokemon": p.name}), null, true); + + const allowedPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + + if (!allowedPokemon.length) { + // If there are no longer any legal pokemon in the party, game over. + this.scene.clearPhaseQueue(); + this.scene.unshiftPhase(new GameOverPhase(this.scene)); + } else if (allowedPokemon.length >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !allowedPokemon[0].isActive(true))) { + // If there is at least one pokemon in the back that is legal to switch in, force a switch. + p.switchOut(false, true); + } else { + // If there are no pokemon in the back but we're not game overing, just hide the pokemon. + // This should only happen in double battles. + p.hideInfo(); + p.setVisible(false); + this.scene.field.remove(p); + this.scene.triggerPokemonFormChange(p, SpeciesFormChangeActiveTrigger, true); + } + if (allowedPokemon.length === 1 && this.scene.currentBattle.double) { + this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); + } + } + }); + //this.scene.pushPhase(new MoveAnimTestPhase(this.scene)); this.scene.eventTarget.dispatchEvent(new TurnInitEvent()); @@ -1776,9 +1836,15 @@ export class CommandPhase extends FieldPhase { super.start(); if (this.fieldIndex) { - const allyCommand = this.scene.currentBattle.turnCommands[this.fieldIndex - 1]; - if (allyCommand.command === Command.BALL || allyCommand.command === Command.RUN) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: allyCommand.command, skip: true }; + // If we somehow are attempting to check the right pokemon but there's only one pokemon out + // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching + if (this.scene.getPlayerField().filter(p => p.isActive()).length === 1) { + this.fieldIndex = FieldPosition.CENTER; + } else { + const allyCommand = this.scene.currentBattle.turnCommands[this.fieldIndex - 1]; + if (allyCommand.command === Command.BALL || allyCommand.command === Command.RUN) { + this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: allyCommand.command, skip: true }; + } } } @@ -2353,7 +2419,7 @@ export class BattleEndPhase extends BattlePhase { } } - for (const pokemon of this.scene.getParty().filter(p => !p.isFainted())) { + for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) { applyPostBattleAbAttrs(PostBattleAbAttr, pokemon); } @@ -3486,7 +3552,7 @@ export class DamagePhase extends PokemonPhase { this.scene.setFieldScale(0.75); this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); this.scene.currentBattle.double = true; - const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()); + const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); if (availablePartyMembers.length > 1) { this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true)); if (!availablePartyMembers[1].isOnField()) { @@ -3568,11 +3634,11 @@ export class FaintPhase extends PokemonPhase { } if (this.player) { - const nonFaintedPartyMembers = this.scene.getParty().filter(p => !p.isFainted()); - const nonFaintedPartyMemberCount = nonFaintedPartyMembers.length; + const nonFaintedLegalPartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const nonFaintedPartyMemberCount = nonFaintedLegalPartyMembers.length; if (!nonFaintedPartyMemberCount) { this.scene.unshiftPhase(new GameOverPhase(this.scene)); - } else if (nonFaintedPartyMemberCount >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !nonFaintedPartyMembers[0].isActive(true))) { + } else if (nonFaintedPartyMemberCount >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !nonFaintedLegalPartyMembers[0].isActive(true))) { this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false)); } if (nonFaintedPartyMemberCount === 1 && this.scene.currentBattle.double) { @@ -3966,7 +4032,7 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => { this.scene.pushPhase(new EncounterPhase(this.scene, true)); - const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()).length; + const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; this.scene.pushPhase(new SummonPhase(this.scene, 0)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { @@ -4018,6 +4084,10 @@ export class GameOverPhase extends BattlePhase { this.scene.clearPhaseQueue(); this.scene.ui.clearText(); + if (this.victory && this.scene.gameMode.isChallenge) { + this.scene.gameMode.challenges.forEach(c => this.scene.validateAchvs(ChallengeAchv, c)); + } + const clear = (endCardPhase?: EndCardPhase) => { if (newClear) { this.handleUnlocks(); @@ -4221,17 +4291,17 @@ export class SwitchPhase extends BattlePhase { super.start(); // Skip modal switch if impossible - if (this.isModal && !this.scene.getParty().filter(p => !p.isFainted() && !p.isActive(true)).length) { + if (this.isModal && !this.scene.getParty().filter(p => !p.isAllowedInBattle() && !p.isActive(true)).length) { return super.end(); } // Check if there is any space still in field - if (this.isModal && this.scene.getPlayerField().filter(p => !p.isFainted() && p.isActive(true)).length >= this.scene.currentBattle.getBattlerCount()) { + if (this.isModal && this.scene.getPlayerField().filter(p => !p.isAllowedInBattle() && p.isActive(true)).length >= this.scene.currentBattle.getBattlerCount()) { return super.end(); } - // Override field index to 0 in case of double battle where 2/3 remaining party members fainted at once - const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => !p.isFainted()).length > 1 ? this.fieldIndex : 0; + // Override field index to 0 in case of double battle where 2/3 remaining legal party members fainted at once + const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? this.fieldIndex : 0; this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { @@ -4917,7 +4987,8 @@ export class SelectModifierPhase extends BattlePhase { let cost: integer; switch (rowCursor) { case 0: - if (!cursor) { + switch (cursor) { + case 0: const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); if (this.scene.money < rerollCost) { this.scene.ui.playError(); @@ -4932,18 +5003,25 @@ export class SelectModifierPhase extends BattlePhase { this.scene.animateMoneyChanged(false); this.scene.playSound("buy"); } - } else if (cursor === 1) { + break; + case 1: this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => { if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && (m as PokemonHeldItemModifier).getTransferrable(true) && (m as PokemonHeldItemModifier).pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; + && (m as PokemonHeldItemModifier).getTransferrable(true) && (m as PokemonHeldItemModifier).pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; const itemModifier = itemModifiers[itemIndex]; this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); } else { this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); } }, PartyUiHandler.FilterItemMaxStacks); - } else { + break; + case 2: + this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => { + this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); + }); + break; + case 3: this.scene.lockModifierTiers = !this.scene.lockModifierTiers; const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 5356c94fe7f..58153b0c64f 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -222,6 +222,7 @@ declare module "i18next" { berry: BerryTranslationEntries; achv: AchievementTranslationEntries; gameStatsUiHandler: SimpleTranslationEntries; + challenges: SimpleTranslationEntries; voucher: SimpleTranslationEntries; biome: SimpleTranslationEntries; pokemonInfoContainer: SimpleTranslationEntries; diff --git a/src/system/achv.ts b/src/system/achv.ts index 364b7e0c579..c63e2101c18 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -3,6 +3,7 @@ import BattleScene from "../battle-scene"; import { TurnHeldItemTransferModifier } from "../modifier/modifier"; import i18next from "../plugins/i18n"; import * as Utils from "../utils"; +import { Challenge, SingleGenerationChallenge, SingleTypeChallenge } from "#app/data/challenge.js"; export enum AchvTier { COMMON, @@ -126,6 +127,12 @@ export class ModifierAchv extends Achv { } } +export class ChallengeAchv extends Achv { + constructor(localizationKey: string, name: string, description: string, iconImage: string, score: integer, challengeFunc: (challenge: Challenge) => boolean) { + super(localizationKey, name, description, iconImage, score, (_scene: BattleScene, args: any[]) => challengeFunc((args[0] as Challenge))); + } +} + /** * Get the description of an achievement from the localization file with all the necessary variables filled in @@ -214,6 +221,43 @@ export function getAchievementDescription(localizationKey: string): string { return i18next.t("achv:PERFECT_IVS.description"); case "CLASSIC_VICTORY": return i18next.t("achv:CLASSIC_VICTORY.description"); + case "MONO_GEN_ONE": + return i18next.t("achv:MONO_GEN_ONE.description"); + case "MONO_GEN_TWO": + return i18next.t("achv:MONO_GEN_TWO.description"); + case "MONO_GEN_THREE": + return i18next.t("achv:MONO_GEN_THREE.description"); + case "MONO_GEN_FOUR": + return i18next.t("achv:MONO_GEN_FOUR.description"); + case "MONO_GEN_FIVE": + return i18next.t("achv:MONO_GEN_FIVE.description"); + case "MONO_GEN_SIX": + return i18next.t("achv:MONO_GEN_SIX.description"); + case "MONO_GEN_SEVEN": + return i18next.t("achv:MONO_GEN_SEVEN.description"); + case "MONO_GEN_EIGHT": + return i18next.t("achv:MONO_GEN_EIGHT.description"); + case "MONO_GEN_NINE": + return i18next.t("achv:MONO_GEN_NINE.description"); + case "MONO_NORMAL": + case "MONO_FIGHTING": + case "MONO_FLYING": + case "MONO_POISON": + case "MONO_GROUND": + case "MONO_ROCK": + case "MONO_BUG": + case "MONO_GHOST": + case "MONO_STEEL": + case "MONO_FIRE": + case "MONO_WATER": + case "MONO_GRASS": + case "MONO_ELECTRIC": + case "MONO_PSYCHIC": + case "MONO_ICE": + case "MONO_DRAGON": + case "MONO_DARK": + case "MONO_FAIRY": + return i18next.t("achv:MonoType.description", {"type": i18next.t(`pokemonInfo:Type.${localizationKey.slice(5)}`)}); default: return ""; } @@ -261,6 +305,33 @@ export const achvs = { HIDDEN_ABILITY: new Achv("HIDDEN_ABILITY","", "HIDDEN_ABILITY.description", "ability_charm", 75), PERFECT_IVS: new Achv("PERFECT_IVS","", "PERFECT_IVS.description", "blunder_policy", 100), CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY","", "CLASSIC_VICTORY.description", "relic_crown", 150), + MONO_GEN_ONE_VICTORY: new ChallengeAchv("MONO_GEN_ONE","", "MONO_GEN_ONE.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 1), + MONO_GEN_TWO_VICTORY: new ChallengeAchv("MONO_GEN_TWO","", "MONO_GEN_TWO.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 2), + MONO_GEN_THREE_VICTORY: new ChallengeAchv("MONO_GEN_THREE","", "MONO_GEN_THREE.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 3), + MONO_GEN_FOUR_VICTORY: new ChallengeAchv("MONO_GEN_FOUR","", "MONO_GEN_FOUR.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 4), + MONO_GEN_FIVE_VICTORY: new ChallengeAchv("MONO_GEN_FIVE","", "MONO_GEN_FIVE.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 5), + MONO_GEN_SIX_VICTORY: new ChallengeAchv("MONO_GEN_SIX","", "MONO_GEN_SIX.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 6), + MONO_GEN_SEVEN_VICTORY: new ChallengeAchv("MONO_GEN_SEVEN","", "MONO_GEN_SEVEN.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 7), + MONO_GEN_EIGHT_VICTORY: new ChallengeAchv("MONO_GEN_EIGHT","", "MONO_GEN_EIGHT.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 8), + MONO_GEN_NINE_VICTORY: new ChallengeAchv("MONO_GEN_NINE","", "MONO_GEN_NINE.description", "mystic_ticket", 100, c => c instanceof SingleGenerationChallenge && c.value === 9), + MONO_NORMAL: new ChallengeAchv("MONO_NORMAL","", "MONO_NORMAL.description", "silk_scarf", 100, c => c instanceof SingleTypeChallenge && c.value === 1), + MONO_FIGHTING: new ChallengeAchv("MONO_FIGHTING","", "MONO_FIGHTING.description", "black_belt", 100, c => c instanceof SingleTypeChallenge && c.value === 2), + MONO_FLYING: new ChallengeAchv("MONO_FLYING","", "MONO_FLYING.description", "sharp_beak", 100, c => c instanceof SingleTypeChallenge && c.value === 3), + MONO_POISON: new ChallengeAchv("MONO_POISON","", "MONO_POISON.description", "poison_barb", 100, c => c instanceof SingleTypeChallenge && c.value === 4), + MONO_GROUND: new ChallengeAchv("MONO_GROUND","", "MONO_GROUND.description", "soft_sand", 100, c => c instanceof SingleTypeChallenge && c.value === 5), + MONO_ROCK: new ChallengeAchv("MONO_ROCK","", "MONO_ROCK.description", "hard_stone", 100, c => c instanceof SingleTypeChallenge && c.value === 6), + MONO_BUG: new ChallengeAchv("MONO_BUG","", "MONO_BUG.description", "silver_powder", 100, c => c instanceof SingleTypeChallenge && c.value === 7), + MONO_GHOST: new ChallengeAchv("MONO_GHOST","", "MONO_GHOST.description", "spell_tag", 100, c => c instanceof SingleTypeChallenge && c.value === 8), + MONO_STEEL: new ChallengeAchv("MONO_STEEL","", "MONO_STEEL.description", "metal_coat", 100, c => c instanceof SingleTypeChallenge && c.value === 9), + MONO_FIRE: new ChallengeAchv("MONO_FIRE","", "MONO_FIRE.description", "charcoal", 100, c => c instanceof SingleTypeChallenge && c.value === 10), + MONO_WATER: new ChallengeAchv("MONO_WATER","", "MONO_WATER.description", "mystic_water", 100, c => c instanceof SingleTypeChallenge && c.value === 11), + MONO_GRASS: new ChallengeAchv("MONO_GRASS","", "MONO_GRASS.description", "miracle_seed", 100, c => c instanceof SingleTypeChallenge && c.value === 12), + MONO_ELECTRIC: new ChallengeAchv("MONO_ELECTRIC","", "MONO_ELECTRIC.description", "magnet", 100, c => c instanceof SingleTypeChallenge && c.value === 13), + MONO_PSYCHIC: new ChallengeAchv("MONO_PSYCHIC","", "MONO_PSYCHIC.description", "twisted_spoon", 100, c => c instanceof SingleTypeChallenge && c.value === 14), + MONO_ICE: new ChallengeAchv("MONO_ICE","", "MONO_ICE.description", "never_melt_ice", 100, c => c instanceof SingleTypeChallenge && c.value === 15), + MONO_DRAGON: new ChallengeAchv("MONO_DRAGON","", "MONO_DRAGON.description", "dragon_fang", 100, c => c instanceof SingleTypeChallenge && c.value === 16), + MONO_DARK: new ChallengeAchv("MONO_DARK","", "MONO_DARK.description", "black_glasses", 100, c => c instanceof SingleTypeChallenge && c.value === 17), + MONO_FAIRY: new ChallengeAchv("MONO_FAIRY","", "MONO_FAIRY.description", "fairy_feather", 100, c => c instanceof SingleTypeChallenge && c.value === 18), }; export function initAchievements() { diff --git a/src/system/challenge-data.ts b/src/system/challenge-data.ts new file mode 100644 index 00000000000..69df11dd395 --- /dev/null +++ b/src/system/challenge-data.ts @@ -0,0 +1,17 @@ +import { Challenge, copyChallenge } from "#app/data/challenge.js"; + +export default class ChallengeData { + public id: integer; + public value: integer; + public severity: integer; + + constructor(source: Challenge | any) { + this.id = source.id; + this.value = source.value; + this.severity = source.severity; + } + + toChallenge(): Challenge { + return copyChallenge(this); + } +} diff --git a/src/system/game-data.ts b/src/system/game-data.ts index c5e2cd6eb91..a6070c6330f 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -9,7 +9,7 @@ import PokemonData from "./pokemon-data"; import PersistentModifierData from "./modifier-data"; import ArenaData from "./arena-data"; import { Unlockables } from "./unlockables"; -import { GameModes, gameModes } from "../game-mode"; +import { GameModes, getGameMode } from "../game-mode"; import { BattleType } from "../battle"; import TrainerData from "./trainer-data"; import { trainerConfigs } from "../data/trainer-config"; @@ -38,6 +38,7 @@ import { EnemyAttackStatusEffectChanceModifier } from "../modifier/modifier"; import { StatusEffect } from "#app/data/status-effect.js"; import { PlayerGender } from "#app/data/enums/player-gender"; import { GameDataType } from "#app/data/enums/game-data-type"; +import ChallengeData from "./challenge-data"; const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary @@ -107,6 +108,7 @@ export interface SessionSaveData { trainer: TrainerData; gameVersion: string; timestamp: integer; + challenges: ChallengeData[]; } interface Unlocks { @@ -780,7 +782,8 @@ export class GameData { battleType: scene.currentBattle.battleType, trainer: scene.currentBattle.battleType === BattleType.TRAINER ? new TrainerData(scene.currentBattle.trainer) : null, gameVersion: scene.game.config.gameVersion, - timestamp: new Date().getTime() + timestamp: new Date().getTime(), + challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)) } as SessionSaveData; } @@ -829,7 +832,10 @@ export class GameData { const initSessionFromData = async sessionData => { console.debug(sessionData); - scene.gameMode = gameModes[sessionData.gameMode || GameModes.CLASSIC]; + scene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC); + if (sessionData.challenges) { + scene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge()); + } scene.setSeed(sessionData.seed || scene.game.config.seed[0]); scene.resetSeed(); @@ -1075,6 +1081,17 @@ export class GameData { return new ArenaData(v); } + if (k === "challenges") { + const ret: ChallengeData[] = []; + if (v === null) { + v = []; + } + for (const c of v) { + ret.push(new ChallengeData(c)); + } + return ret; + } + return v; }) as SessionSaveData; } diff --git a/src/system/unlockables.ts b/src/system/unlockables.ts index f94e29ff039..d8e7d97edf6 100644 --- a/src/system/unlockables.ts +++ b/src/system/unlockables.ts @@ -1,4 +1,4 @@ -import { GameModes, gameModes } from "../game-mode"; +import { GameMode, GameModes } from "../game-mode"; export enum Unlockables { ENDLESS_MODE, @@ -9,10 +9,10 @@ export enum Unlockables { export function getUnlockableName(unlockable: Unlockables) { switch (unlockable) { case Unlockables.ENDLESS_MODE: - return `${gameModes[GameModes.ENDLESS].getName()} Mode`; + return `${GameMode.getModeName(GameModes.ENDLESS)} Mode`; case Unlockables.MINI_BLACK_HOLE: return "Mini Black Hole"; case Unlockables.SPLICED_ENDLESS_MODE: - return `${gameModes[GameModes.SPLICED_ENDLESS].getName()} Mode`; + return `${GameMode.getModeName(GameModes.SPLICED_ENDLESS)} Mode`; } } diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index 521b56a8db2..5a747eb8749 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -35,6 +35,7 @@ import GameManager from "#app/test/utils/gameManager"; import Phaser from "phaser"; import {allSpecies} from "#app/data/pokemon-species"; import {PlayerGender} from "#app/data/enums/player-gender"; +import { getGameMode } from "#app/game-mode.js"; describe("Test Battle Phase", () => { let phaserGame: Phaser.Game; @@ -229,8 +230,9 @@ describe("Test Battle Phase", () => { await game.phaseInterceptor.mustRun(SelectGenderPhase).catch((error) => expect(error).toBe(SelectGenderPhase)); await game.phaseInterceptor.mustRun(TitlePhase).catch((error) => expect(error).toBe(TitlePhase)); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { + game.scene.gameMode = getGameMode(GameModes.CLASSIC); const starters = generateStarter(game.scene); - const selectStarterPhase = new SelectStarterPhase(game.scene, GameModes.CLASSIC); + const selectStarterPhase = new SelectStarterPhase(game.scene); game.scene.pushPhase(new EncounterPhase(game.scene, false)); selectStarterPhase.initBattle(starters); }); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index cc759fd1563..cf38fbe01bb 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -17,7 +17,7 @@ import BattleScene from "#app/battle-scene.js"; import PhaseInterceptor from "#app/test/utils/phaseInterceptor"; import TextInterceptor from "#app/test/utils/TextInterceptor"; import {expect} from "vitest"; -import {GameModes} from "#app/game-mode"; +import {GameModes, getGameMode} from "#app/game-mode"; import fs from "fs"; import { AES, enc } from "crypto-js"; import {updateUserInfo} from "#app/account"; @@ -121,8 +121,9 @@ export default class GameManager { return new Promise(async(resolve) => { await this.runToTitle(); this.onNextPrompt("TitlePhase", Mode.TITLE, () => { + this.scene.gameMode = getGameMode(GameModes.CLASSIC); const starters = generateStarter(this.scene, species); - const selectStarterPhase = new SelectStarterPhase(this.scene, GameModes.CLASSIC); + const selectStarterPhase = new SelectStarterPhase(this.scene); this.scene.pushPhase(new EncounterPhase(this.scene, false)); selectStarterPhase.initBattle(starters); }); diff --git a/src/test/utils/gameManagerUtils.ts b/src/test/utils/gameManagerUtils.ts index 4a72006faa6..1b7bbdabaf0 100644 --- a/src/test/utils/gameManagerUtils.ts +++ b/src/test/utils/gameManagerUtils.ts @@ -3,7 +3,7 @@ import {getDailyRunStarters} from "#app/data/daily-run"; import {Gender} from "#app/data/gender"; import {Species} from "#app/data/enums/species"; import {Starter} from "#app/ui/starter-select-ui-handler"; -import {GameModes, gameModes} from "#app/game-mode"; +import {GameModes, getGameMode} from "#app/game-mode"; import {getPokemonSpecies, getPokemonSpeciesForm} from "#app/data/pokemon-species"; import {PlayerPokemon} from "#app/field/pokemon"; @@ -49,7 +49,7 @@ function getTestRunStarters(scene, seed, species) { return getDailyRunStarters(scene, seed); } const starters: Starter[] = []; - const startingLevel = gameModes[GameModes.CLASSIC].getStartingLevel(); + const startingLevel = getGameMode(GameModes.CLASSIC).getStartingLevel(); for (const specie of species) { const starterSpeciesForm = getPokemonSpeciesForm(specie, 0); diff --git a/src/ui/challenges-select-ui-handler.ts b/src/ui/challenges-select-ui-handler.ts new file mode 100644 index 00000000000..092d954eae7 --- /dev/null +++ b/src/ui/challenges-select-ui-handler.ts @@ -0,0 +1,330 @@ +import BattleScene from "../battle-scene"; +import { TextStyle, addTextObject } from "./text"; +import { Mode } from "./ui"; +import UiHandler from "./ui-handler"; +import { addWindow } from "./ui-theme"; +import {Button} from "../enums/buttons"; +import i18next from "#app/plugins/i18n.js"; +import { SelectStarterPhase, TitlePhase } from "#app/phases.js"; +import { Challenge } from "#app/data/challenge.js"; + +/** + * Handles all the UI for choosing optional challenges. + */ +export default class GameChallengesUiHandler extends UiHandler { + private challengesContainer: Phaser.GameObjects.Container; + private valuesContainer: Phaser.GameObjects.Container; + + private scrollCursor: integer; + + private optionsBg: Phaser.GameObjects.NineSlice; + + // private difficultyText: Phaser.GameObjects.Text; + + private descriptionText: Phaser.GameObjects.Text; + + private challengeLabels: Phaser.GameObjects.Text[]; + private challengeValueLabels: Phaser.GameObjects.Text[]; + + private cursorObj: Phaser.GameObjects.NineSlice; + + private startCursor: Phaser.GameObjects.NineSlice; + + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + } + + setup() { + const ui = this.getUi(); + + this.challengesContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); + + this.challengesContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + + // TODO: Change this back to /9 when adding in difficulty + const headerBg = addWindow(this.scene, 0, 0, (this.scene.game.canvas.width / 6), 24); + headerBg.setOrigin(0, 0); + + const headerText = addTextObject(this.scene, 0, 0, i18next.t("challenges:title"), TextStyle.SETTINGS_LABEL); + headerText.setOrigin(0, 0); + headerText.setPositionRelative(headerBg, 8, 4); + + // const difficultyBg = addWindow(this.scene, 0, 0, (this.scene.game.canvas.width / 18) - 2, 24); + // difficultyBg.setOrigin(0, 0); + // difficultyBg.setPositionRelative(headerBg, headerBg.width, 0); + + // this.difficultyText = addTextObject(this.scene, 0, 0, "0", TextStyle.SETTINGS_LABEL); + // this.difficultyText.setOrigin(0, 0); + // this.difficultyText.setPositionRelative(difficultyBg, 8, 4); + + // const difficultyName = addTextObject(this.scene, 0, 0, i18next.t("challenges:points"), TextStyle.SETTINGS_LABEL); + // difficultyName.setOrigin(0, 0); + // difficultyName.setPositionRelative(difficultyBg, difficultyBg.width - difficultyName.displayWidth - 8, 4); + + this.optionsBg = addWindow(this.scene, 0, headerBg.height, (this.scene.game.canvas.width / 9), (this.scene.game.canvas.height / 6) - headerBg.height - 2); + this.optionsBg.setOrigin(0, 0); + + const descriptionBg = addWindow(this.scene, 0, headerBg.height, (this.scene.game.canvas.width / 18) - 2, (this.scene.game.canvas.height / 6) - headerBg.height - 26); + descriptionBg.setOrigin(0, 0); + descriptionBg.setPositionRelative(this.optionsBg, this.optionsBg.width, 0); + + this.descriptionText = addTextObject(this.scene, 0, 0, "", TextStyle.SETTINGS_LABEL); + this.descriptionText.setOrigin(0, 0); + this.descriptionText.setWordWrapWidth(500, true); + this.descriptionText.setPositionRelative(descriptionBg, 6, 4); + + const startBg = addWindow(this.scene, 0, 0, descriptionBg.width, 24); + startBg.setOrigin(0, 0); + startBg.setPositionRelative(descriptionBg, 0, descriptionBg.height); + + const startText = addTextObject(this.scene, 0, 0, i18next.t("challenges:start"), TextStyle.SETTINGS_LABEL); + startText.setOrigin(0, 0); + startText.setPositionRelative(startBg, 8, 4); + + this.startCursor = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 18) - 10, 16, 1, 1, 1, 1); + this.startCursor.setOrigin(0, 0); + this.startCursor.setPositionRelative(startBg, 4, 4); + this.startCursor.setVisible(false); + + this.valuesContainer = this.scene.add.container(0, 0); + + this.challengeLabels = []; + this.challengeValueLabels = []; + + for (let i = 0; i < 9; i++) { + this.challengeLabels[i] = addTextObject(this.scene, 8, 28 + i * 16, "", TextStyle.SETTINGS_LABEL); + this.challengeLabels[i].setOrigin(0, 0); + + this.valuesContainer.add(this.challengeLabels[i]); + + this.challengeValueLabels[i] = addTextObject(this.scene, 0, 28 + i * 16, "", TextStyle.SETTINGS_LABEL); + this.challengeValueLabels[i].setPositionRelative(this.challengeLabels[i], 100, 0); + + this.valuesContainer.add(this.challengeValueLabels[i]); + } + + this.challengesContainer.add(headerBg); + this.challengesContainer.add(headerText); + // this.challengesContainer.add(difficultyBg); + // this.challengesContainer.add(this.difficultyText); + // this.challengesContainer.add(difficultyName); + this.challengesContainer.add(this.optionsBg); + this.challengesContainer.add(descriptionBg); + this.challengesContainer.add(this.descriptionText); + this.challengesContainer.add(startBg); + this.challengesContainer.add(startText); + this.challengesContainer.add(this.startCursor); + this.challengesContainer.add(this.valuesContainer); + + ui.add(this.challengesContainer); + + this.setCursor(0); + this.setScrollCursor(0); + + this.challengesContainer.setVisible(false); + } + + + updateText(): void { + if (this.scene.gameMode.challenges.length > 0) { + this.descriptionText.text = this.getActiveChallenge().getDescription(); + this.descriptionText.updateText(); + } + + // const totalDifficulty = this.scene.gameMode.challenges.reduce((v, c) => v + c.getDifficulty(), 0); + // const totalMinDifficulty = this.scene.gameMode.challenges.reduce((v, c) => v + c.getMinDifficulty(), 0); + // this.difficultyText.text = `${totalDifficulty}` + (totalMinDifficulty ? `/${totalMinDifficulty}` : ""); + // this.difficultyText.updateText(); + + for (let i = 0; i < this.challengeLabels.length; i++) { + if (i + this.scrollCursor < this.scene.gameMode.challenges.length) { + this.challengeLabels[i].setVisible(true); + this.challengeValueLabels[i].setVisible(true); + this.challengeLabels[i].text = this.scene.gameMode.challenges[i + this.scrollCursor].getName(); + this.challengeValueLabels[i].text = this.scene.gameMode.challenges[i + this.scrollCursor].getValue(); + this.challengeLabels[i].updateText(); + this.challengeValueLabels[i].updateText(); + } else { + this.challengeLabels[i].setVisible(false); + this.challengeValueLabels[i].setVisible(false); + } + } + } + + show(args: any[]): boolean { + super.show(args); + + this.startCursor.setVisible(false); + this.challengesContainer.setVisible(true); + this.setCursor(0); + + this.updateText(); + + this.getUi().moveTo(this.challengesContainer, this.getUi().length - 1); + + this.getUi().hideTooltip(); + + return true; + } + + /** + * Processes input from a specified button. + * This method handles navigation through a UI menu, including movement through menu items + * and handling special actions like cancellation. Each button press may adjust the cursor + * position or the menu scroll, and plays a sound effect if the action was successful. + * + * @param button - The button pressed by the user. + * @returns `true` if the action associated with the button was successfully processed, `false` otherwise. + */ + processInput(button: Button): boolean { + const ui = this.getUi(); + // Defines the maximum number of rows that can be displayed on the screen. + const rowsToDisplay = 9; + + let success = false; + + if (button === Button.CANCEL) { + if (this.startCursor.visible) { + this.startCursor.setVisible(false); + this.cursorObj?.setVisible(true); + } else { + this.scene.clearPhaseQueue(); + this.scene.pushPhase(new TitlePhase(this.scene)); + this.scene.getCurrentPhase().end(); + } + success = true; + } else if (button === Button.SUBMIT || button === Button.ACTION) { + if (this.startCursor.visible) { + const totalDifficulty = this.scene.gameMode.challenges.reduce((v, c) => v + c.getDifficulty(), 0); + const totalMinDifficulty = this.scene.gameMode.challenges.reduce((v, c) => v + c.getMinDifficulty(), 0); + if (totalDifficulty >= totalMinDifficulty) { + this.scene.unshiftPhase(new SelectStarterPhase(this.scene)); + this.scene.getCurrentPhase().end(); + success = true; + } else { + success = false; + } + } else { + this.startCursor.setVisible(true); + this.cursorObj?.setVisible(false); + success = true; + } + } else { + switch (button) { + case Button.UP: + if (this.cursor === 0) { + if (this.scrollCursor === 0) { + // When at the top of the menu and pressing UP, move to the bottommost item. + if (this.scene.gameMode.challenges.length > rowsToDisplay) { // If there are more than 9 challenges, scroll to the bottom + // First, set the cursor to the last visible element, preparing for the scroll to the end. + const successA = this.setCursor(rowsToDisplay - 1); + // Then, adjust the scroll to display the bottommost elements of the menu. + const successB = this.setScrollCursor(this.scene.gameMode.challenges.length - rowsToDisplay); + success = successA && successB; // success is just there to play the little validation sound effect + } else { // If there are 9 or less challenges, just move to the bottom one + success = this.setCursor(this.scene.gameMode.challenges.length - 1); + } + } else { + success = this.setScrollCursor(this.scrollCursor - 1); + } + } else { + success = this.setCursor(this.cursor - 1); + } + if (success) { + this.updateText(); + } + break; + case Button.DOWN: + if (this.cursor === rowsToDisplay - 1) { + if (this.scrollCursor < this.scene.gameMode.challenges.length - rowsToDisplay) { + // When at the bottom and pressing DOWN, scroll if possible. + success = this.setScrollCursor(this.scrollCursor + 1); + } else { + // When at the bottom of a scrolling menu and pressing DOWN, move to the topmost item. + // First, set the cursor to the first visible element, preparing for the scroll to the top. + const successA = this.setCursor(0); + // Then, adjust the scroll to display the topmost elements of the menu. + const successB = this.setScrollCursor(0); + success = successA && successB; // success is just there to play the little validation sound effect + } + } else if (this.scene.gameMode.challenges.length < rowsToDisplay && this.cursor === this.scene.gameMode.challenges.length - 1) { + // When at the bottom of a non-scrolling menu and pressing DOWN, move to the topmost item. + success = this.setCursor(0); + } else { + success = this.setCursor(this.cursor + 1); + } + if (success) { + this.updateText(); + } + break; + case Button.LEFT: + // Moves the option cursor left, if possible. + success = this.getActiveChallenge().decreaseValue(); + if (success) { + this.updateText(); + } + break; + case Button.RIGHT: + // Moves the option cursor right, if possible. + success = this.getActiveChallenge().increaseValue(); + if (success) { + this.updateText(); + } + break; + } + } + + // Plays a select sound effect if an action was successfully processed. + if (success) { + ui.playSelect(); + } + + return success; + } + + setCursor(cursor: integer): boolean { + let ret = super.setCursor(cursor); + + if (!this.cursorObj) { + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 9) - 10, 16, 1, 1, 1, 1); + this.cursorObj.setOrigin(0, 0); + this.valuesContainer.add(this.cursorObj); + } + + ret ||= !this.cursorObj.visible; + this.cursorObj.setVisible(true); + + this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16); + + return ret; + } + + setScrollCursor(scrollCursor: integer): boolean { + if (scrollCursor === this.scrollCursor) { + return false; + } + + this.scrollCursor = scrollCursor; + + this.setCursor(this.cursor); + + return true; + } + + getActiveChallenge(): Challenge { + return this.scene.gameMode.challenges[this.cursor + this.scrollCursor]; + } + + clear() { + super.clear(); + this.challengesContainer.setVisible(false); + this.eraseCursor(); + } + + eraseCursor() { + if (this.cursorObj) { + this.cursorObj.destroy(); + } + this.cursorObj = null; + } +} diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 8eb0bb7b82a..9a17f6e344d 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -18,6 +18,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { private rerollButtonContainer: Phaser.GameObjects.Container; private lockRarityButtonContainer: Phaser.GameObjects.Container; private transferButtonContainer: Phaser.GameObjects.Container; + private checkButtonContainer: Phaser.GameObjects.Container; private rerollCostText: Phaser.GameObjects.Text; private lockRarityButtonText: Phaser.GameObjects.Text; private moveInfoOverlay : MoveInfoOverlay; @@ -45,7 +46,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.modifierContainer = this.scene.add.container(0, 0); ui.add(this.modifierContainer); - this.transferButtonContainer = this.scene.add.container((this.scene.game.canvas.width / 6) - 1, -64); + this.transferButtonContainer = this.scene.add.container((this.scene.game.canvas.width / 6) - 71, -64); this.transferButtonContainer.setName("container-transfer-btn"); this.transferButtonContainer.setVisible(false); ui.add(this.transferButtonContainer); @@ -55,6 +56,16 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { transferButtonText.setOrigin(1, 0); this.transferButtonContainer.add(transferButtonText); + this.checkButtonContainer = this.scene.add.container((this.scene.game.canvas.width / 6) - 1, -64); + this.checkButtonContainer.setName("container-use-btn"); + this.checkButtonContainer.setVisible(false); + ui.add(this.checkButtonContainer); + + const checkButtonText = addTextObject(this.scene, -4, -2, "Check Team", TextStyle.PARTY); + checkButtonText.setName("text-use-btn"); + checkButtonText.setOrigin(1, 0); + this.checkButtonContainer.add(checkButtonText); + this.rerollButtonContainer = this.scene.add.container(16, -64); this.rerollButtonContainer.setName("container-reroll-brn"); this.rerollButtonContainer.setVisible(false); @@ -121,6 +132,9 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.transferButtonContainer.setVisible(false); this.transferButtonContainer.setAlpha(0); + this.checkButtonContainer.setVisible(false); + this.checkButtonContainer.setAlpha(0); + this.rerollButtonContainer.setVisible(false); this.rerollButtonContainer.setAlpha(0); @@ -204,12 +218,14 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { } this.rerollButtonContainer.setAlpha(0); + this.checkButtonContainer.setAlpha(0); this.lockRarityButtonContainer.setAlpha(0); this.rerollButtonContainer.setVisible(true); + this.checkButtonContainer.setVisible(true); this.lockRarityButtonContainer.setVisible(canLockRarities); this.scene.tweens.add({ - targets: [ this.rerollButtonContainer, this.lockRarityButtonContainer ], + targets: [ this.rerollButtonContainer, this.lockRarityButtonContainer, this.checkButtonContainer ], alpha: 1, duration: 250 }); @@ -267,7 +283,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { } else { switch (button) { case Button.UP: - if (!this.rowCursor && this.cursor === 2) { + if (this.rowCursor === 0 && this.cursor === 3) { success = this.setCursor(0); } else if (this.rowCursor < this.shopOptionsRows.length + 1) { success = this.setRowCursor(this.rowCursor + 1); @@ -276,13 +292,29 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { case Button.DOWN: if (this.rowCursor) { success = this.setRowCursor(this.rowCursor - 1); - } else if (this.lockRarityButtonContainer.visible && !this.cursor) { - success = this.setCursor(2); + } else if (this.lockRarityButtonContainer.visible && this.cursor === 0) { + success = this.setCursor(3); } break; case Button.LEFT: if (!this.rowCursor) { - success = this.cursor === 1 && this.rerollButtonContainer.visible && this.setCursor(0); + switch (this.cursor) { + case 0: + success = false; + break; + case 1: + success = this.rerollButtonContainer.visible && this.setCursor(0); + break; + case 2: + if (this.transferButtonContainer.visible) { + success = this.setCursor(1); + } else if (this.rerollButtonContainer.visible) { + success = this.setCursor(0); + } else { + success = false; + } + break; + } } else if (this.cursor) { success = this.setCursor(this.cursor - 1); } else if (this.rowCursor === 1 && this.rerollButtonContainer.visible) { @@ -291,7 +323,21 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { break; case Button.RIGHT: if (!this.rowCursor) { - success = this.cursor !== 1 && this.transferButtonContainer.visible && this.setCursor(1); + switch (this.cursor) { + case 0: + if (this.transferButtonContainer.visible) { + success = this.setCursor(1); + } else { + success = this.setCursor(2); + } + break; + case 1: + success = this.setCursor(2); + break; + case 2: + success = false; + break; + } } else if (this.cursor < this.getRowItems(this.rowCursor) - 1) { success = this.setCursor(this.cursor + 1); } else if (this.rowCursor === 1 && this.transferButtonContainer.visible) { @@ -337,12 +383,15 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { // prepare the move overlay to be shown with the toggle this.moveInfoOverlay.show(allMoves[type.moveId]); } - } else if (!cursor) { + } else if (cursor === 0) { this.cursorObj.setPosition(6, this.lockRarityButtonContainer.visible ? -72 : -60); ui.showText("Spend money to reroll your item options."); } else if (cursor === 1) { - this.cursorObj.setPosition((this.scene.game.canvas.width / 6) - 50, -60); + this.cursorObj.setPosition((this.scene.game.canvas.width / 6) - 120, -60); ui.showText("Transfer a held item from one Pokémon to another."); + } else if (cursor === 2) { + this.cursorObj.setPosition((this.scene.game.canvas.width / 6) - 60, -60); + ui.showText("Check your team or use a form changing item."); } else { this.cursorObj.setPosition(6, -60); ui.showText("Lock item rarities on reroll (affects reroll cost)."); @@ -354,14 +403,15 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { setRowCursor(rowCursor: integer): boolean { const lastRowCursor = this.rowCursor; - if (rowCursor !== lastRowCursor && (rowCursor || this.rerollButtonContainer.visible || this.transferButtonContainer.visible)) { + if (rowCursor !== lastRowCursor) { this.rowCursor = rowCursor; let newCursor = Math.round(this.cursor / Math.max(this.getRowItems(lastRowCursor) - 1, 1) * (this.getRowItems(rowCursor) - 1)); - if (!rowCursor) { - if (!newCursor && !this.rerollButtonContainer.visible) { + if (rowCursor === 0) { + if (newCursor === 0 && !this.rerollButtonContainer.visible) { newCursor = 1; - } else if (newCursor && !this.transferButtonContainer.visible) { - newCursor = 0; + } + if (newCursor === 1 && !this.transferButtonContainer.visible) { + newCursor = 2; } } this.cursor = -1; @@ -375,7 +425,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { private getRowItems(rowCursor: integer): integer { switch (rowCursor) { case 0: - return 2; + return 3; case 1: return this.options.length; default: @@ -437,7 +487,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { onComplete: () => options.forEach(o => o.destroy()) }); - [ this.rerollButtonContainer, this.transferButtonContainer, this.lockRarityButtonContainer ].forEach(container => { + [ this.rerollButtonContainer, this.checkButtonContainer, this.transferButtonContainer, this.lockRarityButtonContainer ].forEach(container => { if (container.visible) { this.scene.tweens.add({ targets: container, diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 425a9dd788d..9192ffc47bc 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -1,4 +1,4 @@ -import { CommandPhase } from "../phases"; +import { CommandPhase, SelectModifierPhase } from "../phases"; import BattleScene from "../battle-scene"; import { PlayerPokemon, PokemonMove } from "../field/pokemon"; import { addTextObject, TextStyle } from "./text"; @@ -17,6 +17,7 @@ import { addWindow } from "./ui-theme"; import { SpeciesFormChangeItemTrigger } from "../data/pokemon-forms"; import { getVariantTint } from "#app/data/variant"; import {Button} from "../enums/buttons"; +import { applyChallenges, ChallengeType } from "#app/data/challenge.js"; import MoveInfoOverlay from "./move-info-overlay"; import i18next from "i18next"; @@ -33,7 +34,8 @@ export enum PartyUiMode { REMEMBER_MOVE_MODIFIER, MODIFIER_TRANSFER, SPLICE, - RELEASE + RELEASE, + CHECK } export enum PartyOption { @@ -120,6 +122,20 @@ export default class PartyUiHandler extends MessageUiHandler { return null; }; + /** + * For consistency reasons, this looks like the above filters. However this is used only internally and is always enforced for switching. + * @param pokemon The pokemon to check. + * @returns + */ + private FilterChallengeLegal = (pokemon: PlayerPokemon) => { + const challengeAllowed = new Utils.BooleanHolder(true); + applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, pokemon, challengeAllowed); + if (!challengeAllowed.value) { + return `${pokemon.name} can't be used in\nthis challenge!`; + } + return null; + }; + private static FilterAllMoves = (_pokemonMove: PokemonMove) => null; public static FilterItemMaxStacks = (pokemon: PlayerPokemon, modifier: PokemonHeldItemModifier) => { @@ -280,6 +296,9 @@ export default class PartyUiHandler extends MessageUiHandler { let filterResult: string; if (option !== PartyOption.TRANSFER && option !== PartyOption.SPLICE) { filterResult = (this.selectFilter as PokemonSelectFilter)(pokemon); + if (filterResult === null && (option === PartyOption.SEND_OUT || option === PartyOption.PASS_BATON)) { + filterResult = this.FilterChallengeLegal(pokemon); + } if (filterResult === null && this.partyUiMode === PartyUiMode.MOVE_MODIFIER) { filterResult = this.moveSelectFilter(pokemon.moveset[this.optionsCursor]); } @@ -293,7 +312,7 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.partyUiMode !== PartyUiMode.SPLICE) { this.clearOptions(); } - if (this.selectCallback) { + if (this.selectCallback && this.partyUiMode !== PartyUiMode.CHECK) { if (option === PartyOption.TRANSFER) { if (this.transferCursor !== this.cursor) { (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, this.transferOptionCursor, this.transferQuantities[this.transferOptionCursor], this.cursor); @@ -315,11 +334,8 @@ export default class PartyUiHandler extends MessageUiHandler { selectCallback(this.cursor, option); } } else { - if (option >= PartyOption.FORM_CHANGE_ITEM && this.scene.getCurrentPhase() instanceof CommandPhase) { - switch (this.partyUiMode) { - case PartyUiMode.SWITCH: - case PartyUiMode.FAINT_SWITCH: - case PartyUiMode.POST_BATTLE_SWITCH: + if (option >= PartyOption.FORM_CHANGE_ITEM && this.scene.getCurrentPhase() instanceof SelectModifierPhase) { + if (this.partyUiMode === PartyUiMode.CHECK) { let formChangeItemModifiers = this.scene.findModifiers(m => m instanceof PokemonFormChangeItemModifier && m.pokemonId === pokemon.id) as PokemonFormChangeItemModifier[]; if (formChangeItemModifiers.find(m => m.active)) { formChangeItemModifiers = formChangeItemModifiers.filter(m => m.active); @@ -327,7 +343,6 @@ export default class PartyUiHandler extends MessageUiHandler { const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; modifier.active = !modifier.active; this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger, false, true); - break; } } else if (this.cursor) { (this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, this.cursor, option === PartyOption.PASS_BATON); @@ -690,15 +705,6 @@ export default class PartyUiHandler extends MessageUiHandler { this.options.push(PartyOption.PASS_BATON); } } - if (this.scene.getCurrentPhase() instanceof CommandPhase) { - formChangeItemModifiers = this.scene.findModifiers(m => m instanceof PokemonFormChangeItemModifier && m.pokemonId === pokemon.id) as PokemonFormChangeItemModifier[]; - if (formChangeItemModifiers.find(m => m.active)) { - formChangeItemModifiers = formChangeItemModifiers.filter(m => m.active); - } - for (let i = 0; i < formChangeItemModifiers.length; i++) { - this.options.push(PartyOption.FORM_CHANGE_ITEM + i); - } - } break; case PartyUiMode.REVIVAL_BLESSING: this.options.push(PartyOption.REVIVE); @@ -724,6 +730,17 @@ export default class PartyUiHandler extends MessageUiHandler { case PartyUiMode.RELEASE: this.options.push(PartyOption.RELEASE); break; + case PartyUiMode.CHECK: + if (this.scene.getCurrentPhase() instanceof SelectModifierPhase) { + formChangeItemModifiers = this.scene.findModifiers(m => m instanceof PokemonFormChangeItemModifier && m.pokemonId === pokemon.id) as PokemonFormChangeItemModifier[]; + if (formChangeItemModifiers.find(m => m.active)) { + formChangeItemModifiers = formChangeItemModifiers.filter(m => m.active); + } + for (let i = 0; i < formChangeItemModifiers.length; i++) { + this.options.push(PartyOption.FORM_CHANGE_ITEM + i); + } + } + break; } this.options.push(PartyOption.SUMMARY); diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index d5dda05a519..4e6d3daecbe 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -1,5 +1,5 @@ import BattleScene from "../battle-scene"; -import { gameModes } from "../game-mode"; +import { GameMode } from "../game-mode"; import { SessionSaveData } from "../system/game-data"; import { TextStyle, addTextObject } from "./text"; import { Mode } from "./ui"; @@ -266,7 +266,7 @@ class SessionSlot extends Phaser.GameObjects.Container { async setupWithData(data: SessionSaveData) { this.remove(this.loadingLabel, true); - const gameModeLabel = addTextObject(this.scene, 8, 5, `${gameModes[data.gameMode]?.getName() || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); + const gameModeLabel = addTextObject(this.scene, 8, 5, `${GameMode.getModeName(data.gameMode) || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); this.add(gameModeLabel); const timestampLabel = addTextObject(this.scene, 8, 19, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 16406fad675..597d7321567 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -18,8 +18,8 @@ import { LevelMoves, pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "../ import PokemonSpecies, { allSpecies, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { Type } from "../data/type"; import { Button } from "../enums/buttons"; -import { GameModes, gameModes } from "../game-mode"; -import { TitlePhase } from "../phases"; +import { GameModes } from "../game-mode"; +import { SelectChallengePhase, TitlePhase } from "../phases"; import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterFormMoveData, StarterMoveset } from "../system/game-data"; import { Passive as PassiveAttr } from "#app/data/enums/passive"; import { Tutorial, handleTutorial } from "../tutorial"; @@ -31,6 +31,7 @@ import { StatsContainer } from "./stats-container"; import { TextStyle, addBBCodeTextObject, addTextObject } from "./text"; import { Mode } from "./ui"; import { addWindow } from "./ui-theme"; +import * as Challenge from "../data/challenge"; import MoveInfoOverlay from "./move-info-overlay"; export type StarterSelectCallback = (starters: Starter[]) => void; @@ -245,7 +246,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private iconAnimHandler: PokemonIconAnimHandler; private starterSelectCallback: StarterSelectCallback; - private gameMode: GameModes; protected blockInput: boolean = false; @@ -724,14 +724,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { show(args: any[]): boolean { this.moveInfoOverlay.clear(); // clear this when removing a menu; the cancel button doesn't seem to trigger this automatically on controllers - if (args.length >= 2 && args[0] instanceof Function && typeof args[1] === "number") { + if (args.length >= 1 && args[0] instanceof Function) { super.show(args); this.starterSelectCallback = args[0] as StarterSelectCallback; this.starterSelectContainer.setVisible(true); - this.gameMode = args[1]; - this.setGenMode(false); this.setCursor(0); this.setGenMode(true); @@ -957,7 +955,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } else { this.blockInput = true; this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); + if (this.scene.gameMode.isChallenge) { + this.scene.pushPhase(new SelectChallengePhase(this.scene)); + } else { + this.scene.pushPhase(new TitlePhase(this.scene)); + } this.scene.getCurrentPhase().end(); success = true; } @@ -1029,7 +1031,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } const species = this.genSpecies[this.getGenCursorWithScroll()][this.cursor]; - if (!isDupe && this.tryUpdateValue(this.scene.gameData.getSpeciesStarterValue(species.speciesId))) { + + const isValidForChallenge = new Utils.BooleanHolder(true); + Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, species, isValidForChallenge); + + if (!isDupe && isValidForChallenge.value && this.tryUpdateValue(this.scene.gameData.getSpeciesStarterValue(species.speciesId))) { const cursorObj = this.starterCursorObjs[this.starterCursors.length]; cursorObj.setVisible(true); cursorObj.setPosition(this.cursorObj.x, this.cursorObj.y); @@ -1521,13 +1527,18 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } getValueLimit(): integer { - switch (this.gameMode) { + const valueLimit = new Utils.IntegerHolder(0); + switch (this.scene.gameMode.modeId) { case GameModes.ENDLESS: case GameModes.SPLICED_ENDLESS: - return 15; + valueLimit.value = 15; default: - return 10; + valueLimit.value = 10; } + + Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_POINTS, valueLimit); + + return valueLimit.value; } setCursor(cursor: integer): boolean { @@ -2160,20 +2171,25 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const speciesSprite = this.starterSelectGenIconContainers[g].getAt(s) as Phaser.GameObjects.Sprite; /** - * If remainValue greater than or equal pokemon species, the user can select. + * If remainValue greater than or equal pokemon species and the pokemon is legal for this challenge, the user can select. * so that the alpha value of pokemon sprite set 1. * * If speciesStarterDexEntry?.caughtAttr is true, this species registered in stater. * we change to can AddParty value to true since the user has enough cost to choose this pokemon and this pokemon registered too. */ - if (remainValue >= speciesStarterValue) { + const isValidForChallenge = new Utils.BooleanHolder(true); + Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, this.genSpecies[g][s], isValidForChallenge); + + const canBeChosen = remainValue >= speciesStarterValue && isValidForChallenge.value; + + if (canBeChosen) { speciesSprite.setAlpha(1); if (speciesStarterDexEntry?.caughtAttr) { this.canAddParty = true; } } else { /** - * If remainValue less than pokemon, the use can't select. + * If it can't be chosen, the user can't select. * so that the alpha value of pokemon sprite set 0.375. */ speciesSprite.setAlpha(0.375); @@ -2202,8 +2218,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { ui.showText(i18next.t("starterSelectUiHandler:confirmStartTeam"), null, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => { - const startRun = (gameMode: GameModes) => { - this.scene.gameMode = gameModes[gameMode]; + const startRun = () => { this.scene.money = this.scene.gameMode.getStartingMoney(); ui.setMode(Mode.STARTER_SELECT); const thisObj = this; @@ -2222,7 +2237,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { }; })); }; - startRun(this.gameMode); + startRun(); }, cancel, null, null, 19); }); diff --git a/src/ui/ui-handler.ts b/src/ui/ui-handler.ts index 74015ed7245..e74853276ea 100644 --- a/src/ui/ui-handler.ts +++ b/src/ui/ui-handler.ts @@ -3,12 +3,19 @@ import { TextStyle, getTextColor } from "./text"; import { Mode } from "./ui"; import {Button} from "../enums/buttons"; +/** + * A basic abstract class to act as a holder and processor for UI elements. + */ export default abstract class UiHandler { protected scene: BattleScene; protected mode: integer; protected cursor: integer = 0; public active: boolean = false; + /** + * @param {BattleScene} scene The same scene as everything else. + * @param {Mode} mode The mode of the UI element. These should be unique. + */ constructor(scene: BattleScene, mode: Mode) { this.scene = scene; this.mode = mode; diff --git a/src/ui/ui.ts b/src/ui/ui.ts index aaf764f501f..366be949374 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -14,6 +14,7 @@ import EvolutionSceneHandler from "./evolution-scene-handler"; import TargetSelectUiHandler from "./target-select-ui-handler"; import SettingsUiHandler from "./settings/settings-ui-handler"; import SettingsGamepadUiHandler from "./settings/settings-gamepad-ui-handler"; +import GameChallengesUiHandler from "./challenges-select-ui-handler"; import { TextStyle, addTextObject } from "./text"; import AchvBar from "./achv-bar"; import MenuUiHandler from "./menu-ui-handler"; @@ -80,7 +81,8 @@ export enum Mode { LOADING, SESSION_RELOAD, UNAVAILABLE, - OUTDATED + OUTDATED, + CHALLENGE_SELECT } const transitionModes = [ @@ -91,7 +93,8 @@ const transitionModes = [ Mode.EVOLUTION_SCENE, Mode.EGG_HATCH_SCENE, Mode.EGG_LIST, - Mode.EGG_GACHA + Mode.EGG_GACHA, + Mode.CHALLENGE_SELECT ]; const noTransitionModes = [ @@ -173,7 +176,8 @@ export default class UI extends Phaser.GameObjects.Container { new LoadingModalUiHandler(scene), new SessionReloadModalUiHandler(scene), new UnavailableModalUiHandler(scene), - new OutdatedModalUiHandler(scene) + new OutdatedModalUiHandler(scene), + new GameChallengesUiHandler(scene) ]; }