Add tracking for nuzlocke completion

This commit is contained in:
Sirz Benjie 2025-08-09 15:25:42 -05:00
parent 6c0253ada4
commit fec202c103
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
12 changed files with 458 additions and 137 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View File

@ -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;
}

View File

@ -103,3 +103,12 @@ export type CoerceNullPropertiesToUndefined<T extends object> = {
* @typeParam T - The type to render partial
*/
export type AtLeastOne<T extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, 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<B> {
private __brand: B;
}

View File

@ -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 },
];

View File

@ -47,6 +47,7 @@ export class EggHatchData {
caughtCount: currDexEntry.caughtCount,
hatchedCount: currDexEntry.hatchedCount,
ivs: [...currDexEntry.ivs],
ribbons: currDexEntry.ribbons,
};
this.starterDataEntryBeforeUpdate = {
moveset: currStarterDataEntry.moveset,

View File

@ -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");

View File

@ -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

View File

@ -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(),
};

View File

@ -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<boolean> {
return new Promise<boolean>(resolve => {
try {
let systemData = this.parseSystemData(systemDataStr);
const { promise, resolve } = Promise.withResolvers<boolean>();
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);
}
}
}

210
src/system/ribbon-data.ts Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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;
}