diff --git a/public/images/ui/champion_ribbon_emerald.png b/public/images/ui/champion_ribbon_emerald.png new file mode 100644 index 00000000000..81b111a05a9 Binary files /dev/null and b/public/images/ui/champion_ribbon_emerald.png differ diff --git a/src/@types/dex-data.ts b/src/@types/dex-data.ts index 88cc16886bd..167ba525af5 100644 --- a/src/@types/dex-data.ts +++ b/src/@types/dex-data.ts @@ -1,3 +1,5 @@ +import type { RibbonData } from "#system/ribbon-data"; + export interface DexData { [key: number]: DexEntry; } @@ -10,4 +12,5 @@ export interface DexEntry { caughtCount: number; hatchedCount: number; ivs: number[]; + ribbons: RibbonData; } diff --git a/src/@types/helpers/type-helpers.ts b/src/@types/helpers/type-helpers.ts index 7ad20b88956..0be391aa3c4 100644 --- a/src/@types/helpers/type-helpers.ts +++ b/src/@types/helpers/type-helpers.ts @@ -103,3 +103,12 @@ export type CoerceNullPropertiesToUndefined = { * @typeParam T - The type to render partial */ export type AtLeastOne = Partial & ObjectValues<{ [K in keyof T]: Pick, K> }>; + +/** Type helper that adds a brand to a type, used for nominal typing. + * + * @remarks + * Brands should be either a string or unique symbol. This prevents overlap with other types. + */ +export declare class Brander { + private __brand: B; +} diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 724d1f302da..1157b47a515 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -20,6 +20,7 @@ import { Trainer } from "#field/trainer"; import type { ModifierTypeOption } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; import type { DexAttrProps, GameData } from "#system/game-data"; +import { RibbonData, type RibbonFlag } from "#system/ribbon-data"; import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; import { deepCopy } from "#utils/data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -42,6 +43,15 @@ export abstract class Challenge { public conditions: ChallengeCondition[]; + /** + * The Ribbon awarded on challenge completion, or 0 if the challenge has no ribbon. + * + * @defaultValue 0 + */ + public get ribbonAwarded(): RibbonFlag { + return 0 as RibbonFlag; + } + /** * @param id {@link Challenges} The enum value for the challenge */ @@ -423,6 +433,9 @@ type ChallengeCondition = (data: GameData) => boolean; * Implements a mono generation challenge. */ export class SingleGenerationChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + return RibbonData.MONO_GEN; + } constructor() { super(Challenges.SINGLE_GENERATION, 9); } @@ -686,6 +699,12 @@ interface monotypeOverride { * Implements a mono type challenge. */ export class SingleTypeChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + // `this.value` represents the 1-based index of pokemon type + // `RibbonData.MONO_NORMAL` starts the flag position for the types, + // and we shift it by 1 for the specific type. + return (RibbonData.MONO_NORMAL << (this.value - 1)) as RibbonFlag; + } private static TYPE_OVERRIDES: monotypeOverride[] = [ { species: SpeciesId.CASTFORM, type: PokemonType.NORMAL, fusion: false }, ]; diff --git a/src/data/egg-hatch-data.ts b/src/data/egg-hatch-data.ts index 6aead19eb7f..e78dc4d7984 100644 --- a/src/data/egg-hatch-data.ts +++ b/src/data/egg-hatch-data.ts @@ -47,6 +47,7 @@ export class EggHatchData { caughtCount: currDexEntry.caughtCount, hatchedCount: currDexEntry.hatchedCount, ivs: [...currDexEntry.ivs], + ribbons: currDexEntry.ribbons, }; this.starterDataEntryBeforeUpdate = { moveset: currStarterDataEntry.moveset, diff --git a/src/loading-scene.ts b/src/loading-scene.ts index d2b4a76ef10..fda9c3cd094 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -90,6 +90,7 @@ export class LoadingScene extends SceneBase { this.loadAtlas("shiny_icons", "ui"); this.loadImage("ha_capsule", "ui", "ha_capsule.png"); this.loadImage("champion_ribbon", "ui", "champion_ribbon.png"); + this.loadImage("champion_ribbon_emerald", "ui", "champion_ribbon_emerald.png"); this.loadImage("icon_spliced", "ui"); this.loadImage("icon_lock", "ui", "icon_lock.png"); this.loadImage("icon_stop", "ui", "icon_stop.png"); diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index d4562b5a237..49b61ad78f2 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -1,7 +1,7 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { clientSessionId } from "#app/account"; import { globalScene } from "#app/global-scene"; -import { pokemonEvolutions } from "#balance/pokemon-evolutions"; +import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { modifierTypes } from "#data/data-lists"; import { getCharVariantFromDialogue } from "#data/dialogue"; import type { PokemonSpecies } from "#data/pokemon-species"; @@ -19,9 +19,11 @@ import { ChallengeData } from "#system/challenge-data"; import type { SessionSaveData } from "#system/game-data"; import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; +import { RibbonData, type RibbonFlag } from "#system/ribbon-data"; import { TrainerData } from "#system/trainer-data"; import { trainerConfigs } from "#trainers/trainer-config"; -import { isLocal, isLocalServerConnected } from "#utils/common"; +import { checkSpeciesValidForChallenge, isNuzlockeChallenge } from "#utils/challenge-utils"; +import { isLocal, isLocalServerConnected, isNullOrUndefined } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -111,6 +113,50 @@ export class GameOverPhase extends BattlePhase { } } + /** + * Submethod of {@linkcode handleGameOver} that awards ribbons to Pokémon in the player's party based on the current + * game mode and challenges. + */ + private awardRibbons(): void { + let ribbonFlags = 0; + if (globalScene.gameMode.isClassic) { + ribbonFlags |= RibbonData.CLASSIC; + } + if (isNuzlockeChallenge()) { + ribbonFlags |= RibbonData.NUZLOCKE; + } + for (const challenge of globalScene.gameMode.challenges) { + const ribbon = challenge.ribbonAwarded; + if (challenge.value && ribbon) { + ribbonFlags |= ribbon; + } + } + // Award ribbons to all Pokémon in the player's party that are considered valid + // for the current game mode and challenges. + for (const pokemon of globalScene.getPlayerParty()) { + if ( + checkSpeciesValidForChallenge( + pokemon.species, + globalScene.gameData.getSpeciesDexAttrProps(pokemon.species, pokemon.getDexAttr()), + false, + ) + ) { + const speciesId = pokemon.species.speciesId; + const dexData = globalScene.gameData.dexData; + dexData[speciesId].ribbons.award(ribbonFlags as RibbonFlag); + + // Mark all pre-evolutions of the Pokémon with the same ribbon flags. + for ( + let prevoId = pokemonPrevolutions[speciesId]; + !isNullOrUndefined(prevoId); + prevoId = pokemonPrevolutions[prevoId] + ) { + dexData[speciesId].ribbons.award(ribbonFlags as RibbonFlag); + } + } + } + } + handleGameOver(): void { const doGameOver = (newClear: boolean) => { globalScene.disableMenu = true; @@ -122,12 +168,12 @@ export class GameOverPhase extends BattlePhase { globalScene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY); globalScene.gameData.gameStats.sessionsWon++; for (const pokemon of globalScene.getPlayerParty()) { - this.awardRibbon(pokemon); - + this.awardFirstClassicCompletion(pokemon); if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) { - this.awardRibbon(pokemon, true); + this.awardFirstClassicCompletion(pokemon, true); } } + this.awardRibbons(); } else if (globalScene.gameMode.isDaily && newClear) { globalScene.gameData.gameStats.dailyRunSessionsWon++; } @@ -263,7 +309,7 @@ export class GameOverPhase extends BattlePhase { } } - awardRibbon(pokemon: Pokemon, forStarter = false): void { + awardFirstClassicCompletion(pokemon: Pokemon, forStarter = false): void { const speciesId = getPokemonSpecies(pokemon.species.speciesId); const speciesRibbonCount = globalScene.gameData.incrementRibbonCount(speciesId, forStarter); // first time classic win, award voucher diff --git a/src/system/achv.ts b/src/system/achv.ts index 383d07252e6..8e312e3d590 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -5,7 +5,6 @@ import { FlipStatChallenge, FreshStartChallenge, InverseBattleChallenge, - LimitedCatchChallenge, SingleGenerationChallenge, SingleTypeChallenge, } from "#data/challenge"; @@ -14,6 +13,7 @@ import { PlayerGender } from "#enums/player-gender"; import { getShortenedStatKey, Stat } from "#enums/stat"; import { TurnHeldItemTransferModifier } from "#modifiers/modifier"; import type { ConditionFn } from "#types/common"; +import { isNuzlockeChallenge } from "#utils/challenge-utils"; import { NumberHolder } from "#utils/common"; import i18next from "i18next"; import type { Modifier } from "typescript"; @@ -924,18 +924,7 @@ export const achvs = { globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0), ).setSecret(), // TODO: Decide on icon - NUZLOCKE: new ChallengeAchv( - "NUZLOCKE", - "", - "NUZLOCKE.description", - "leaf_stone", - 100, - c => - c instanceof LimitedCatchChallenge && - c.value > 0 && - globalScene.gameMode.challenges.some(c => c.id === Challenges.HARDCORE && c.value > 0) && - globalScene.gameMode.challenges.some(c => c.id === Challenges.FRESH_START && c.value > 0), - ), + NUZLOCKE: new ChallengeAchv("NUZLOCKE", "", "NUZLOCKE.description", "leaf_stone", 100, isNuzlockeChallenge), BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(), }; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index ae559072e35..0bc92dfa57e 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -48,6 +48,7 @@ import { EggData } from "#system/egg-data"; import { GameStats } from "#system/game-stats"; import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; +import { RibbonData } from "#system/ribbon-data"; import { resetSettings, SettingKeys, setSetting } from "#system/settings"; import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad"; import type { SettingKeyboard } from "#system/settings-keyboard"; @@ -399,121 +400,121 @@ export class GameData { } public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise { - return new Promise(resolve => { - try { - let systemData = this.parseSystemData(systemDataStr); + const { promise, resolve } = Promise.withResolvers(); + try { + let systemData = this.parseSystemData(systemDataStr); - if (cachedSystemDataStr) { - const cachedSystemData = this.parseSystemData(cachedSystemDataStr); - if (cachedSystemData.timestamp > systemData.timestamp) { - console.debug("Use cached system"); - systemData = cachedSystemData; - systemDataStr = cachedSystemDataStr; - } else { - this.clearLocalData(); - } - } - - console.debug(systemData); - - localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin)); - - const lsItemKey = `runHistoryData_${loggedInUser?.username}`; - const lsItem = localStorage.getItem(lsItemKey); - if (!lsItem) { - localStorage.setItem(lsItemKey, ""); - } - - applySystemVersionMigration(systemData); - - this.trainerId = systemData.trainerId; - this.secretId = systemData.secretId; - - this.gender = systemData.gender; - - this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); - - if (!systemData.starterData) { - this.initStarterData(); - - if (systemData["starterMoveData"]) { - const starterMoveData = systemData["starterMoveData"]; - for (const s of Object.keys(starterMoveData)) { - this.starterData[s].moveset = starterMoveData[s]; - } - } - - if (systemData["starterEggMoveData"]) { - const starterEggMoveData = systemData["starterEggMoveData"]; - for (const s of Object.keys(starterEggMoveData)) { - this.starterData[s].eggMoves = starterEggMoveData[s]; - } - } - - this.migrateStarterAbilities(systemData, this.starterData); - - const starterIds = Object.keys(this.starterData).map(s => Number.parseInt(s) as SpeciesId); - for (const s of starterIds) { - this.starterData[s].candyCount += systemData.dexData[s].caughtCount; - this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2; - if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) { - this.starterData[s].candyCount += 4; - } - } + if (cachedSystemDataStr) { + const cachedSystemData = this.parseSystemData(cachedSystemDataStr); + if (cachedSystemData.timestamp > systemData.timestamp) { + console.debug("Use cached system"); + systemData = cachedSystemData; + systemDataStr = cachedSystemDataStr; } else { - this.starterData = systemData.starterData; + this.clearLocalData(); } - - if (systemData.gameStats) { - this.gameStats = systemData.gameStats; - } - - if (systemData.unlocks) { - for (const key of Object.keys(systemData.unlocks)) { - if (this.unlocks.hasOwnProperty(key)) { - this.unlocks[key] = systemData.unlocks[key]; - } - } - } - - if (systemData.achvUnlocks) { - for (const a of Object.keys(systemData.achvUnlocks)) { - if (achvs.hasOwnProperty(a)) { - this.achvUnlocks[a] = systemData.achvUnlocks[a]; - } - } - } - - if (systemData.voucherUnlocks) { - for (const v of Object.keys(systemData.voucherUnlocks)) { - if (vouchers.hasOwnProperty(v)) { - this.voucherUnlocks[v] = systemData.voucherUnlocks[v]; - } - } - } - - if (systemData.voucherCounts) { - getEnumKeys(VoucherType).forEach(key => { - const index = VoucherType[key]; - this.voucherCounts[index] = systemData.voucherCounts[index] || 0; - }); - } - - this.eggs = systemData.eggs ? systemData.eggs.map(e => e.toEgg()) : []; - - this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0]; - this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0]; - - this.dexData = Object.assign(this.dexData, systemData.dexData); - this.consolidateDexData(this.dexData); - this.defaultDexData = null; - - resolve(true); - } catch (err) { - console.error(err); - resolve(false); } - }); + + console.debug(systemData); + + localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin)); + + const lsItemKey = `runHistoryData_${loggedInUser?.username}`; + const lsItem = localStorage.getItem(lsItemKey); + if (!lsItem) { + localStorage.setItem(lsItemKey, ""); + } + + applySystemVersionMigration(systemData); + + this.trainerId = systemData.trainerId; + this.secretId = systemData.secretId; + + this.gender = systemData.gender; + + this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); + + if (!systemData.starterData) { + this.initStarterData(); + + if (systemData["starterMoveData"]) { + const starterMoveData = systemData["starterMoveData"]; + for (const s of Object.keys(starterMoveData)) { + this.starterData[s].moveset = starterMoveData[s]; + } + } + + if (systemData["starterEggMoveData"]) { + const starterEggMoveData = systemData["starterEggMoveData"]; + for (const s of Object.keys(starterEggMoveData)) { + this.starterData[s].eggMoves = starterEggMoveData[s]; + } + } + + this.migrateStarterAbilities(systemData, this.starterData); + + const starterIds = Object.keys(this.starterData).map(s => Number.parseInt(s) as SpeciesId); + for (const s of starterIds) { + this.starterData[s].candyCount += systemData.dexData[s].caughtCount; + this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2; + if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) { + this.starterData[s].candyCount += 4; + } + } + } else { + this.starterData = systemData.starterData; + } + + if (systemData.gameStats) { + this.gameStats = systemData.gameStats; + } + + if (systemData.unlocks) { + for (const key of Object.keys(systemData.unlocks)) { + if (this.unlocks.hasOwnProperty(key)) { + this.unlocks[key] = systemData.unlocks[key]; + } + } + } + + if (systemData.achvUnlocks) { + for (const a of Object.keys(systemData.achvUnlocks)) { + if (achvs.hasOwnProperty(a)) { + this.achvUnlocks[a] = systemData.achvUnlocks[a]; + } + } + } + + if (systemData.voucherUnlocks) { + for (const v of Object.keys(systemData.voucherUnlocks)) { + if (vouchers.hasOwnProperty(v)) { + this.voucherUnlocks[v] = systemData.voucherUnlocks[v]; + } + } + } + + if (systemData.voucherCounts) { + getEnumKeys(VoucherType).forEach(key => { + const index = VoucherType[key]; + this.voucherCounts[index] = systemData.voucherCounts[index] || 0; + }); + } + + this.eggs = systemData.eggs ? systemData.eggs.map(e => e.toEgg()) : []; + + this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0]; + this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0]; + + this.dexData = Object.assign(this.dexData, systemData.dexData); + this.consolidateDexData(this.dexData); + this.defaultDexData = null; + + resolve(true); + } catch (err) { + console.error(err); + resolve(false); + } + return promise; } /** @@ -624,6 +625,9 @@ export class GameData { } return ret; } + if (k === "ribbons") { + return RibbonData.fromJSON(v); + } return k.endsWith("Attr") && !["natureAttr", "abilityAttr", "passiveAttr"].includes(k) ? BigInt(v ?? 0) : v; }) as SystemSaveData; @@ -1584,6 +1588,7 @@ export class GameData { caughtCount: 0, hatchedCount: 0, ivs: [0, 0, 0, 0, 0, 0], + ribbons: new RibbonData(0), }; } @@ -1828,6 +1833,12 @@ export class GameData { }); } + /** + * Increase the number of classic ribbons won with this species. + * @param species - The species to increment the ribbon count for + * @param forStarter - If true, will increment the ribbon count for the root species of the given species + * @returns The number of classic wins after incrementing. + */ incrementRibbonCount(species: PokemonSpecies, forStarter = false): number { const speciesIdToIncrement: SpeciesId = species.getRootSpeciesId(forStarter); @@ -2127,6 +2138,9 @@ export class GameData { if (!entry.hasOwnProperty("natureAttr") || (entry.caughtAttr && !entry.natureAttr)) { entry.natureAttr = this.defaultDexData?.[k].natureAttr || 1 << randInt(25, 1); } + if (!entry.hasOwnProperty("ribbons")) { + entry.ribbons = new RibbonData(0); + } } } diff --git a/src/system/ribbon-data.ts b/src/system/ribbon-data.ts new file mode 100644 index 00000000000..46bf48c9a8e --- /dev/null +++ b/src/system/ribbon-data.ts @@ -0,0 +1,210 @@ +import type { Brander } from "#types/type-helpers"; +export type RibbonFlag = number & Brander<"RibbonFlag">; + +/** + * Class for ribbon data management. Usually constructed via the {@linkcode fromJSON} method. + * + * @remarks + * Stores information about the ribbons earned by a species using a bitfield. + */ +export class RibbonData { + /** Internal bitfield storing the unlock state for each ribbon */ + private payload: number; + + //#region Ribbons + //#region Monotype challenge ribbons + /** Flag for winning the normal monotype challenge */ + public static readonly MONO_NORMAL = 0x1 as RibbonFlag; + /** Flag for winning the fighting monotype challenge */ + public static readonly MONO_FIGHTING = 0x2 as RibbonFlag; + /** Flag for winning the flying monotype challenge */ + public static readonly MONO_FLYING = 0x4 as RibbonFlag; + /** Flag for winning the poision monotype challenge */ + public static readonly MONO_POISON = 0x8 as RibbonFlag; + /** Flag for winning the ground monotype challenge */ + public static readonly MONO_GROUND = 0x10 as RibbonFlag; + /** Flag for winning the rock monotype challenge */ + public static readonly MONO_ROCK = 0x20 as RibbonFlag; + /** Flag for winning the bug monotype challenge */ + public static readonly MONO_BUG = 0x40 as RibbonFlag; + /** Flag for winning the ghost monotype challenge */ + public static readonly MONO_GHOST = 0x80 as RibbonFlag; + /** Flag for winning the steel monotype challenge */ + public static readonly MONO_STEEL = 0x100 as RibbonFlag; + /** Flag for winning the fire monotype challenge */ + public static readonly MONO_FIRE = 0x200 as RibbonFlag; + /** Flag for winning the water monotype challenge */ + public static readonly MONO_WATER = 0x400 as RibbonFlag; + /** Flag for winning the grass monotype challenge */ + public static readonly MONO_GRASS = 0x800 as RibbonFlag; + /** Flag for winning the electric monotype challenge */ + public static readonly MONO_ELECTRIC = 0x1000 as RibbonFlag; + /** Flag for winning the psychic monotype challenge */ + public static readonly MONO_PSYCHIC = 0x2000 as RibbonFlag; + /** Flag for winning the ice monotype challenge */ + public static readonly MONO_ICE = 0x4000 as RibbonFlag; + /** Flag for winning the dragon monotype challenge */ + public static readonly MONO_DRAGON = 0x8000 as RibbonFlag; + /** Flag for winning the dark monotype challenge */ + public static readonly MONO_DARK = 0x10000 as RibbonFlag; + /** Flag for winning the fairy monotype challenge */ + public static readonly MONO_FAIRY = 0x20000 as RibbonFlag; + //#endregion Monotype ribbons + + /** Flag for winning a mono generation challenge */ + public static readonly MONO_GEN = 0x40000 as RibbonFlag; + + /** Flag for winning classic */ + public static readonly CLASSIC = 0x80000; + /** Flag for winning the nuzzlocke challenge */ + public static readonly NUZLOCKE = 0x80000; + /** Flag for reaching max friendship */ + public static readonly FRIENDSHIP = 0x100000; + //#endregion Ribbons + + constructor(value: number) { + this.payload = value; + } + + /** Serialize the bitfield payload as a hex encoded string */ + public toJSON(): string { + return this.payload.toString(16); + } + + /** + * Decode a hexadecimal string representation of the bitfield into a `RibbonData` instance + * + * @param value - Hexadecimal string representation of the bitfield (without the leading 0x) + * @returns A new instance of `RibbonData` initialized with the provided bitfield. + */ + public static fromJSON(value: string): RibbonData { + try { + return new RibbonData(Number.parseInt(value, 16)); + } catch { + return new RibbonData(0); + } + } + + /** + * Award one or more ribbons to the ribbon data by setting the corresponding flags in the bitfield. + * + * @param flags - The flags to set. Can be a single flag or multiple flags. + */ + public award(...flags: [RibbonFlag, ...RibbonFlag[]]): void { + for (const f of flags) { + this.payload |= f; + } + } + + //#region getters + /** Ribbon for completing the game in classic mode */ + public get classic(): boolean { + return !!(this.payload & RibbonData.CLASSIC); + } + + /** Ribbon for completing a Nuzlocke challenge */ + public get nuzlocke(): boolean { + return !!(this.payload & RibbonData.NUZLOCKE); + } + + /** Ribbon for reaching max friendship with a Pokémon */ + public get maxFriendship(): boolean { + return !!(this.payload & RibbonData.FRIENDSHIP); + } + + /** Ribbon for completing the normal monotype challenge */ + public get monoNormal(): boolean { + return !!(this.payload & RibbonData.MONO_NORMAL); + } + + /** Ribbon for completing the flying monotype challenge */ + public get monoFlying(): boolean { + return !!(this.payload & RibbonData.MONO_FLYING); + } + + /** Ribbon for completing the poison monotype challenge */ + public get monoPoison(): boolean { + return !!(this.payload & RibbonData.MONO_POISON); + } + + /** Ribbon for completing the ground monotype challenge */ + public get monoGround(): boolean { + return !!(this.payload & RibbonData.MONO_GROUND); + } + + /** Ribbon for completing the rock monotype challenge */ + public get monoRock(): boolean { + return !!(this.payload & RibbonData.MONO_ROCK); + } + + /** Ribbon for completing the bug monotype challenge */ + public get monoBug(): boolean { + return !!(this.payload & RibbonData.MONO_BUG); + } + + /** Ribbon for completing the ghost monotype challenge */ + public get monoGhost(): boolean { + return !!(this.payload & RibbonData.MONO_GHOST); + } + + /** Ribbon for completing the steel monotype challenge */ + public get monoSteel(): boolean { + return !!(this.payload & RibbonData.MONO_STEEL); + } + + /** Ribbon for completing the fire monotype challenge */ + public get monoFire(): boolean { + return !!(this.payload & RibbonData.MONO_FIRE); + } + + /** Ribbon for completing the water monotype challenge */ + public get monoWater(): boolean { + return !!(this.payload & RibbonData.MONO_WATER); + } + + /** Ribbon for completing the grass monotype challenge */ + public get monoGrass(): boolean { + return !!(this.payload & RibbonData.MONO_GRASS); + } + + /** Ribbon for completing the electric monotype challenge */ + public get monoElectric(): boolean { + return !!(this.payload & RibbonData.MONO_ELECTRIC); + } + + /** Ribbon for completing the psychic monotype challenge */ + public get monoPsychic(): boolean { + return !!(this.payload & RibbonData.MONO_PSYCHIC); + } + + /** Ribbon for completing the ice monotype challenge */ + public get monoIce(): boolean { + return !!(this.payload & RibbonData.MONO_ICE); + } + + /** Ribbon for completing the dragon monotype challenge */ + public get monoDragon(): boolean { + return !!(this.payload & RibbonData.MONO_DRAGON); + } + + /** Ribbon for completing the dark monotype challenge */ + public get monoDark(): boolean { + return !!(this.payload & RibbonData.MONO_DARK); + } + + /** Ribbon for completing the fairy monotype challenge */ + public get monoFairy(): boolean { + return !!(this.payload & RibbonData.MONO_FAIRY); + } + + /** Ribbon for completing fighting the monotype challenge */ + public get monoFighting(): boolean { + return !!(this.payload & RibbonData.MONO_FIGHTING); + } + + /** Ribbon for completing any monogen challenge */ + public get monoGen(): boolean { + return !!(this.payload & RibbonData.MONO_GEN); + } + //#endregion getters +} diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index fbcc6ae7e32..19663bd8606 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -3226,6 +3226,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { onScreenFirstIndex + maxRows * maxColumns - 1, ); + const gameData = globalScene.gameData; + this.starterSelectScrollBar.setScrollCursor(this.scrollCursor); let pokerusCursorIndex = 0; @@ -3265,9 +3267,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { container.label.setVisible(true); const speciesVariants = - speciesId && globalScene.gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY + speciesId && gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY ? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter( - v => !!(globalScene.gameData.dexData[speciesId].caughtAttr & v), + v => !!(gameData.dexData[speciesId].caughtAttr & v), ) : []; for (let v = 0; v < 3; v++) { @@ -3282,12 +3284,13 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } - container.starterPassiveBgs.setVisible(!!globalScene.gameData.starterData[speciesId].passiveAttr); + container.starterPassiveBgs.setVisible(!!gameData.starterData[speciesId].passiveAttr); container.hiddenAbilityIcon.setVisible( - !!globalScene.gameData.dexData[speciesId].caughtAttr && - !!(globalScene.gameData.starterData[speciesId].abilityAttr & 4), + !!gameData.dexData[speciesId].caughtAttr && !!(gameData.starterData[speciesId].abilityAttr & 4), ); - container.classicWinIcon.setVisible(globalScene.gameData.starterData[speciesId].classicWinCount > 0); + container.classicWinIcon + .setVisible(gameData.starterData[speciesId].classicWinCount > 0) + .setTexture(gameData.dexData[speciesId].ribbons.nuzlocke ? "champion_ribbon_emerald" : "champion_ribbon"); container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false); // 'Candy Icon' mode diff --git a/src/utils/challenge-utils.ts b/src/utils/challenge-utils.ts index 43297027e04..c4fac3a0323 100644 --- a/src/utils/challenge-utils.ts +++ b/src/utils/challenge-utils.ts @@ -4,6 +4,7 @@ import { pokemonEvolutions } from "#balance/pokemon-evolutions"; import { pokemonFormChanges } from "#data/pokemon-forms"; import type { PokemonSpecies } from "#data/pokemon-species"; import { ChallengeType } from "#enums/challenge-type"; +import { Challenges } from "#enums/challenges"; import type { MoveId } from "#enums/move-id"; import type { MoveSourceType } from "#enums/move-source-type"; import type { SpeciesId } from "#enums/species-id"; @@ -378,7 +379,7 @@ export function checkStarterValidForChallenge(species: PokemonSpecies, props: De * @param soft - If `true`, allow it if it could become valid through a form change. * @returns `true` if the species is considered valid. */ -function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { +export function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { const isValidForChallenge = new BooleanHolder(true); applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props); if (!soft || !pokemonFormChanges.hasOwnProperty(species.speciesId)) { @@ -407,3 +408,28 @@ function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrPr }); return result; } + +/** @returns Whether the current game mode meets the criteria to be considered a Nuzlocke challenge */ +export function isNuzlockeChallenge(): boolean { + let isFreshStart = false; + let isLimitedCatch = false; + let isHardcore = false; + for (const challenge of globalScene.gameMode.challenges) { + // value is 0 if challenge is not active + if (!challenge.value) { + continue; + } + switch (challenge.id) { + case Challenges.FRESH_START: + isFreshStart = true; + break; + case Challenges.LIMITED_CATCH: + isLimitedCatch = true; + break; + case Challenges.HARDCORE: + isHardcore = true; + break; + } + } + return isFreshStart && isLimitedCatch && isHardcore; +}