This commit is contained in:
Sirz Benjie 2025-08-10 23:38:21 -05:00 committed by GitHub
commit 1de9000c08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 528 additions and 174 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View File

@ -1,3 +1,5 @@
import type { RibbonData } from "#system/ribbons/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

@ -101,3 +101,9 @@ export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15;
* Default: `10000` (0.01%)
*/
export const FAKE_TITLE_LOGO_CHANCE = 10000;
/**
* The ceiling on friendshp amount that can be reached through the use of rare candies.
* Using rare candies will never increase friendship beyond this value.
*/
export const RARE_CANDY_FRIENDSHIP_CAP = 200;

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/ribbons/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

@ -1,7 +1,7 @@
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
import type { AnySound, BattleScene } from "#app/battle-scene";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants";
import { timedEventManager } from "#app/global-event-manager";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
@ -139,6 +139,8 @@ import { populateVariantColors, variantColorCache, variantData } from "#sprites/
import { achvs } from "#system/achv";
import type { StarterDataEntry, StarterMoveset } from "#system/game-data";
import type { PokemonData } from "#system/pokemon-data";
import { RibbonData } from "#system/ribbons/ribbon-data";
import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods";
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types";
import type { DamageCalculationResult, DamageResult } from "#types/damage-result";
import type { IllusionData } from "#types/illusion-data";
@ -5822,45 +5824,59 @@ export class PlayerPokemon extends Pokemon {
);
});
}
addFriendship(friendship: number): void {
if (friendship > 0) {
const starterSpeciesId = this.species.getRootSpeciesId();
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0;
const starterData = [
globalScene.gameData.starterData[starterSpeciesId],
fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null,
].filter(d => !!d);
const amount = new NumberHolder(friendship);
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
const candyFriendshipMultiplier = globalScene.gameMode.isClassic
? timedEventManager.getClassicFriendshipMultiplier()
: 1;
const fusionReduction = fusionStarterSpeciesId
? timedEventManager.areFusionsBoosted()
? 1.5 // Divide candy gain for fusions by 1.5 during events
: 2 // 2 for fusions outside events
: 1; // 1 for non-fused mons
const starterAmount = new NumberHolder(Math.floor((amount.value * candyFriendshipMultiplier) / fusionReduction));
// Add friendship to this PlayerPokemon
this.friendship = Math.min(this.friendship + amount.value, 255);
if (this.friendship === 255) {
globalScene.validateAchv(achvs.MAX_FRIENDSHIP);
}
// Add to candy progress for this mon's starter species and its fused species (if it has one)
starterData.forEach((sd: StarterDataEntry, i: number) => {
const speciesId = !i ? starterSpeciesId : (fusionStarterSpeciesId as SpeciesId);
sd.friendship = (sd.friendship || 0) + starterAmount.value;
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) {
globalScene.gameData.addStarterCandy(getPokemonSpecies(speciesId), 1);
sd.friendship = 0;
}
});
} else {
// Lose friendship upon fainting
/**
* Add friendship to this Pokemon
*
* @remarks
* This adds friendship to the pokemon's friendship stat (used for evolution, return, etc.) and candy progress.
* For fusions, candy progress for each species in the fusion is halved.
*
* @param friendship - The amount of friendship to add. Negative values will reduce friendship, though not below 0.
* @param capped - If true, don't allow the friendship gain to exceed 200. Used to cap friendship gains from rare candies.
*/
addFriendship(friendship: number, capped = false): void {
// Short-circuit friendship loss, which doesn't impact candy friendship
if (friendship <= 0) {
this.friendship = Math.max(this.friendship + friendship, 0);
return;
}
const starterSpeciesId = this.species.getRootSpeciesId();
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0;
const starterGameData = globalScene.gameData.starterData;
const starterData: [StarterDataEntry, SpeciesId][] = [[starterGameData[starterSpeciesId], starterSpeciesId]];
if (fusionStarterSpeciesId) {
starterData.push([starterGameData[fusionStarterSpeciesId], fusionStarterSpeciesId]);
}
const amount = new NumberHolder(friendship);
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
friendship = amount.value;
const newFriendship = this.friendship + friendship;
// If capped is true, only adjust friendship if the new friendship is less than or equal to 200.
if (!capped || newFriendship <= RARE_CANDY_FRIENDSHIP_CAP) {
this.friendship = Math.min(newFriendship, 255);
if (newFriendship >= 255) {
globalScene.validateAchv(achvs.MAX_FRIENDSHIP);
awardRibbonsToSpeciesLine(this.species.speciesId, RibbonData.FRIENDSHIP);
}
}
let candyFriendshipMultiplier = globalScene.gameMode.isClassic
? timedEventManager.getClassicFriendshipMultiplier()
: 1;
if (fusionStarterSpeciesId) {
candyFriendshipMultiplier /= timedEventManager.areFusionsBoosted() ? 1.5 : 2;
}
const candyFriendshipAmount = Math.floor(friendship * candyFriendshipMultiplier);
// Add to candy progress for this mon's starter species and its fused species (if it has one)
starterData.forEach(([sd, id]: [StarterDataEntry, SpeciesId]) => {
sd.friendship = (sd.friendship || 0) + candyFriendshipAmount;
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[id])) {
globalScene.gameData.addStarterCandy(getPokemonSpecies(id), 1);
sd.friendship = 0;
}
});
}
getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> {

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

@ -2304,7 +2304,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
playerPokemon.levelExp = 0;
}
playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY);
playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY, true);
globalScene.phaseManager.unshiftNew(
"LevelUpPhase",

View File

@ -19,8 +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/ribbons/ribbon-data";
import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods";
import { TrainerData } from "#system/trainer-data";
import { trainerConfigs } from "#trainers/trainer-config";
import { checkSpeciesValidForChallenge, isNuzlockeChallenge } from "#utils/challenge-utils";
import { isLocal, isLocalServerConnected } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next";
@ -111,6 +114,40 @@ 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()) {
const species = pokemon.species;
if (
checkSpeciesValidForChallenge(
species,
globalScene.gameData.getSpeciesDexAttrProps(species, pokemon.getDexAttr()),
false,
)
) {
awardRibbonsToSpeciesLine(species.speciesId, ribbonFlags as RibbonFlag);
}
}
}
handleGameOver(): void {
const doGameOver = (newClear: boolean) => {
globalScene.disableMenu = true;
@ -122,12 +159,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 +300,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/ribbons/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);
}
}
}

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 as RibbonFlag;
/** Flag for winning the nuzzlocke challenge */
public static readonly NUZLOCKE = 0x80000 as RibbonFlag;
/** Flag for reaching max friendship */
public static readonly FRIENDSHIP = 0x100000 as RibbonFlag;
//#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

@ -0,0 +1,20 @@
import { globalScene } from "#app/global-scene";
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
import type { SpeciesId } from "#enums/species-id";
import type { RibbonFlag } from "#system/ribbons/ribbon-data";
import { isNullOrUndefined } from "#utils/common";
/**
* Award one or more ribbons to a species and its pre-evolutions
*
* @param id - The ID of the species to award ribbons to
* @param ribbons The ribbon(s) to award (use bitwise OR to combine multiple)
*/
export function awardRibbonsToSpeciesLine(id: SpeciesId, ribbons: RibbonFlag): void {
const dexData = globalScene.gameData.dexData;
dexData[id].ribbons.award(ribbons);
// Mark all pre-evolutions of the Pokémon with the same ribbon flags.
for (let prevoId = pokemonPrevolutions[id]; !isNullOrUndefined(prevoId); prevoId = pokemonPrevolutions[prevoId]) {
dexData[id].ribbons.award(ribbons);
}
}

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