From 60d0b278f5d5ffafefa6ff33c942bd705d8621b2 Mon Sep 17 00:00:00 2001 From: DuckySoLucky Date: Sun, 8 Dec 2024 17:02:11 +0100 Subject: [PATCH] feat(stats): add museum --- src/hooks.server.ts | 4 +- src/lib/constants/NotEnoughUpdates-REPO | 1 - src/lib/custom_resources.ts | 8 +- src/lib/sections/stats/Inventory.svelte | 7 +- src/lib/server/constants/accessories.ts | 10 +- src/lib/server/constants/museum.ts | 341 ++++++++++++++++++ .../server/constants/update-collections.ts | 3 +- src/lib/server/constants/update-items.ts | 5 +- src/lib/server/db/mongo/update-collections.ts | 4 +- src/lib/server/db/mongo/update-items.ts | 4 +- src/lib/server/helper.ts | 29 +- .../NotEnoughUpdates/parseNEURepository.ts | 2 +- src/lib/server/lib.ts | 26 ++ src/lib/server/stats.ts | 7 +- src/lib/server/stats/items.ts | 19 +- src/lib/server/stats/items/museum.ts | 57 +++ src/lib/server/stats/items/processing.ts | 4 +- src/lib/server/stats/main_stats.ts | 4 +- src/lib/server/stats/museum.ts | 218 +++++++++++ src/lib/types/global.d.ts | 1 + src/lib/types/helper.d.ts | 10 + src/lib/types/processed/profile/items.d.ts | 10 +- src/lib/types/raw/museum/lib.d.ts | 34 ++ src/lib/types/raw/profile/lib.d.ts | 13 + .../api/museum/[paramProfile]/+server.ts | 15 + .../api/stats/[paramPlayer=player]/+server.ts | 5 +- .../[paramProfile]/+server.ts | 5 +- 27 files changed, 800 insertions(+), 46 deletions(-) delete mode 160000 src/lib/constants/NotEnoughUpdates-REPO create mode 100644 src/lib/server/constants/museum.ts create mode 100644 src/lib/server/stats/items/museum.ts create mode 100644 src/lib/server/stats/museum.ts create mode 100644 src/lib/types/raw/museum/lib.d.ts create mode 100644 src/routes/api/museum/[paramProfile]/+server.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts index bd627255e..822f9f611 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -10,11 +10,11 @@ import { startRedis } from "./lib/server/db/redis"; init(); startMongo().then(() => { - console.log("[MONGO] Mongo started!"); + console.log("[MONGO] MongoDB succeesfully connected"); }); startRedis().then(() => { - console.log("[REDIS] Redis started!"); + console.log("[REDIS] Redis succeesfully connected"); }); updateNotEnoughUpdatesRepository().then(() => { diff --git a/src/lib/constants/NotEnoughUpdates-REPO b/src/lib/constants/NotEnoughUpdates-REPO deleted file mode 160000 index a3a65484a..000000000 --- a/src/lib/constants/NotEnoughUpdates-REPO +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a3a65484ae88015b0de76447493522afdb93cc81 diff --git a/src/lib/custom_resources.ts b/src/lib/custom_resources.ts index e6da066b4..819196711 100644 --- a/src/lib/custom_resources.ts +++ b/src/lib/custom_resources.ts @@ -78,8 +78,8 @@ let packConfigHashes: { [key: string]: string } = {}; const outputPacks: OutputResourcePack[] = []; export async function init() { - console.log(`Custom Resources loading started.`); - console.time(`custom_resources`); + console.log(`[CUSTOM-RESOURCES] Custom Resources loading started.`); + const timeNow = performance.now(); await loadPackConfigs(); let resourcesUpToDate = false; @@ -137,8 +137,8 @@ export async function init() { resourcesReady = true; - console.log(`Custom Resources loading done.`); - console.timeEnd(`custom_resources`); + const packs = new Set(resourcePacks.map((pack) => pack.config.id)); + console.log(`[CUSTOM-RESOURCES] Successfully loaded ${packs.size} resource packs in ${(performance.now() - timeNow).toFixed(2)}ms`); } async function loadPackConfigs() { diff --git a/src/lib/sections/stats/Inventory.svelte b/src/lib/sections/stats/Inventory.svelte index 95b5a41f9..0fb2c5c1f 100644 --- a/src/lib/sections/stats/Inventory.svelte +++ b/src/lib/sections/stats/Inventory.svelte @@ -17,6 +17,7 @@ const pots = profile.items.potion_bag; const fish = profile.items.fishing_bag; const quiver = profile.items.quiver; + const museum = profile.items.museum; const openTab = writable("inv"); @@ -66,8 +67,8 @@ { id: "museum", icon: "/api/head/438cf3f8e54afc3b3f91d20a49f324dca1486007fe545399055524c17941f4dc", - items: [], - hr: 45 + items: museum, + hr: 54 } ]; @@ -106,7 +107,7 @@ {#if tab.hr === index}
{/if} - {#if item.id} + {#if item.texture_path}
{#if tab.id === "inv"} diff --git a/src/lib/server/constants/accessories.ts b/src/lib/server/constants/accessories.ts index bb092a75a..9af82860b 100644 --- a/src/lib/server/constants/accessories.ts +++ b/src/lib/server/constants/accessories.ts @@ -1,17 +1,17 @@ // CREDITS: https://github.com/MattTheCuber (Modified) +import { ITEMS } from "$lib/shared/constants/items"; import type { SpecialAccessory, SpecialAccessoryConstant, allAccessories } from "$types/stats"; -import { ITEMS as ALL_ITEMS } from "$lib/shared/constants/items"; -let ITEMS = [] as allAccessories[]; +let ACCESSORIES = [] as allAccessories[]; function getAccessories() { const output = [] as allAccessories[]; - ALL_ITEMS.forEach((item) => { + ITEMS.forEach((item) => { if (item.category === "accessory") { output.push(item as allAccessories); } }); - ITEMS = output; + ACCESSORIES = output; } setTimeout(getAccessories, 60 * 60 * 1000); // 1 hour @@ -138,7 +138,7 @@ export const SPECIAL_ACCESSORIES = { } as Record; export function getAllAccessories() { - const output = ITEMS.reduce((accessory, item) => { + const output = ACCESSORIES.reduce((accessory, item) => { if (ignoredAccessories.includes(item.id)) return accessory; if (Object.values(ACCESSORY_ALIASES).find((list) => list.includes(item.id))) return accessory; diff --git a/src/lib/server/constants/museum.ts b/src/lib/server/constants/museum.ts new file mode 100644 index 000000000..4caeb855a --- /dev/null +++ b/src/lib/server/constants/museum.ts @@ -0,0 +1,341 @@ +import { ITEMS } from "./constants"; + +function sortMuseumItems(museum: MuseumConstants, armorSetId: string) { + return museum.armor_sets[armorSetId].sort((a: string, b: string) => { + const aId = ITEMS.get(a)?.id; + const bId = ITEMS.get(b)?.id; + if (!aId || !bId) { + return 0; + } + + const priorityOrder = ["HAT", "HOOD", "HELMET", "CHESTPLATE", "TUNIC", "LEGGINGS", "TROUSERS", "SLIPPERS", "BOOTS", "NECKLACE", "CLOAK", "BELT", "GAUNTLET", "GLOVES"]; + + return priorityOrder.findIndex((x) => aId.includes(x)) - priorityOrder.findIndex((x) => bId.includes(x)); + }); +} + +async function retrieveMuseumItems() { + const timeNow = Date.now(); + // ! INFO: This is needed, the museum = { ... } doens't work for some reason? + MUSEUM.armor_to_id = {}; + MUSEUM.armor_sets = {}; + MUSEUM.children = {}; + MUSEUM.weapons = []; + MUSEUM.armor = []; + MUSEUM.rarities = []; + + const museumItems = Array.from(ITEMS.values()).filter((item) => item.museum_data !== undefined); + for (const item of museumItems) { + if (!item.museum_data || !item.id) { + continue; + } + + const category = item.museum_data.type.toLowerCase() as keyof MuseumConstants; + if (!MUSEUM[category]) { + console.log(`[MUSEUM] Unknown museum category: ${category}`); + continue; + } + + if (["weapons", "rarities"].includes(category)) { + (MUSEUM[category] as string[]).push(item.id); + } + + if (item.museum_data.parent && Object.keys(item.museum_data.parent).length > 0) { + const [parentKey, parentValue] = Object.entries(item.museum_data.parent)[0]; + MUSEUM.children[parentValue as string] = parentKey; + } + + if (item.museum_data.armor_set_donation_xp) { + const armorSetId = Object.keys(item.museum_data.armor_set_donation_xp)[0]; + if (!armorSetId) { + console.log(`[MUSEUM] Invalid armor set donation XP for ${item.id}`); + continue; + } + + MUSEUM.armor_sets[armorSetId] ??= []; + MUSEUM.armor_sets[armorSetId].push(item.id); + + sortMuseumItems(MUSEUM, armorSetId); + + MUSEUM.armor_to_id[armorSetId] = MUSEUM.armor_sets[armorSetId].at(0)!; + + MUSEUM.armor ??= []; + if (!MUSEUM.armor.includes(armorSetId)) { + MUSEUM.armor.push(armorSetId); + } + } + } + + console.log(`[MUSEUM] Updated museum items in ${Date.now() - timeNow}ms`); +} + +const categoryInventory = [ + { + display_name: "Go Back", + texture_path: "/api/item/ARROW", + rarity: "uncommon", + tag: { + display: { + Lore: [] + } + }, + position: 48 + }, + { + display_name: "Close", + texture_path: "/api/item/BARRIER", + rarity: "special", + tag: { + display: { + Lore: [] + } + }, + position: 49 + }, + { + display_name: "Next Page", + texture_path: "/api/item/ARROW", + rarity: "uncommon", + tag: { + display: { + Lore: [] + } + }, + position: 53 + } +]; + +const MUSEUM: MuseumConstants = { + armor_to_id: {}, + armor_sets: {}, + children: {}, + weapons: [], + armor: [], + rarities: [], + getAllItems: function (): string[] { + return this.weapons.concat(this.armor, this.rarities); + } +}; + +export const MUSEUM_INVENTORY = { + inventory: [ + { + display_name: "Museum", + rarity: "rare", + texture_path: "/api/head/438cf3f8e54afc3b3f91d20a49f324dca1486007fe545399055524c17941f4dc", + tag: { + display: { + Lore: ["§7The §9Museum §7is a compendium", "§7of all of your items in", "§7SkyBlock. Donate items to your", "§7Museum to unlock rewards.", "", "§7Other players can visit your", "§7Museum at any time! Display your", "§7best items proudly for all to", "§7see.", ""] + } + }, + position: 4, + progressType: "total" + }, + { + display_name: "Weapons", + texture_path: "/api/item/DIAMOND_SWORD", + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §6Weapons §7that", "§7you have donated to the", "§7§9Museum§7!", ""] + } + }, + inventoryType: "weapons", + containsItems: [ + { + display_name: "Weapons", + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §6Weapons §7that", "§7you have donated to the", "§7§9Museum§7!", ""] + } + }, + position: 4 + }, + ...categoryInventory + ], + position: 19, + progressType: "weapons" + }, + { + display_name: "Armor Sets", + texture_path: "/api/item/DIAMOND_CHESTPLATE", + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §9Armor Sets", "§9§7that you have donated to the", "§7§9Museum§7!", ""] + } + }, + position: 21, + inventoryType: "armor", + containsItems: [ + { + display_name: "Armor Sets", + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §9Armor Sets", "§9§7that you have donated to the", "§7§9Museum§7!", ""] + } + }, + position: 4 + }, + ...categoryInventory + ], + progressType: "armor" + }, + { + display: "Rarities", + rarity: "uncommon", + texture_path: "/api/head/86addbd5dedad40999473be4a7f48f6236a79a0dce971b5dbd7372014ae394d", + tag: { + display: { + Lore: ["§7View all of the §5Rarities", "§5§7that you have donated to the", "§7§9Museum§7!", ""] + } + }, + position: 23, + inventoryType: "rarities", + containsItems: [ + { + display: "Rarities", + rarity: "uncommon", + texture_path: "/api/head/86addbd5dedad40999473be4a7f48f6236a79a0dce971b5dbd7372014ae394d", + tag: { + display: { + Lore: ["§7View all of the §5Rarities", "§5§7that you have donated to the", "§7§9Museum§7!", ""] + } + }, + position: 4 + }, + ...categoryInventory + ], + progressType: "rarities" + }, + { + display_name: "Special Items", + texture_path: "/api/item/CAKE", + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §dSpecial Items", "§d§7that you have donated to the", "§7§9Museum§7!", "", "§7These items don't count towards", "§7Museum progress and rewards, but", "§7are cool nonetheless. Items that", "§7are §9rare §7and §6prestigious", "§6§7fit into this category, and", "§7can be displayed in the Main", "§7room of the Museum.", ""] + } + }, + position: 25, + inventoryType: "special", + containsItems: [ + { + display_name: "Special Items", + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View all of the §dSpecial Items", "§d§7that you have donated to the", "§7§9Museum§7!", "", "§7These items don't count towards", "§7Museum progress and rewards, but", "§7are cool nonetheless. Items that", "§7are §9rare §7and §6prestigious", "§6§7fit into this category, and", "§7can be displayed in the Main", "§7room of the Museum.", ""] + } + }, + position: 4 + }, + ...categoryInventory + ], + progressType: "special" + }, + { + display_name: "Museum Appraisal", + texture_path: "/api/item/DIAMOND", + rarity: "legendary", + tag: { + display: { + Lore: ["§7§6Madame Goldsworth §7offers an", "§7appraisal service for Museums.", "§7When unlocked, she will appraise", "§7the value of your Museum each", "§7time you add or remove items.", "", "§7This service also allows you to", "§7appear on the §6Top Valued", "§6§7filter in the §9Museum", "§9Browser§7.", ""] + } + }, + position: 40, + progressType: "appraisal" + }, + { + display_name: "Edit NPC Tags", + texture_path: "/api/item/NAME_TAG", + rarity: "uncommon", + tag: { + display: { + Lore: ["§7Edit the tags that appear above", "§7your NPC. Show off your SkyBlock", "§7progress with tags showing your", "§7highest collection, best Skill,", "§7and more!", "", "§cCOMING SOON"] + } + }, + position: 45 + }, + { + display_name: "Museum Rewards", + texture_path: "/api/item/GOLD_BLOCK", + rarity: "legendary", + tag: { + display: { + Lore: ["§7Each time you donate an item to", "§7your Museum, the §bCurator", "§b§7will reward you.", "", "§7§dSpecial Items §7do not count", "§7towards your Museum rewards", "§7progress.", "", "§7Currently, most rewards are", "§7§ccoming soon§7, but you can", "§7view them anyway."] + } + }, + position: 48 + }, + { + display_name: "Close", + texture_path: "/api/item/BARRIER", + rarity: "special", + tag: { + display: { + Lore: [] + } + }, + position: 49 + }, + { + display_name: "Museum Browser", + texture_path: "/api/item/SIGN", + rarity: "uncommon", + tag: { + display: { + Lore: ["§7View the Museums of your", "§7friends, top valued players, and", "§7more!"] + } + }, + position: 50 + } + ], + item_slots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43], + missing_item: { + weapons: { + display_name: null, + rarity: "special", + tag: { + display: { + Lore: ["§7Click on this item in your", "§7inventory to add it to your", "§7§9Museum§7!"] + } + } + }, + armor: { + display_name: null, + rarity: "special", + tag: { + display: { + Lore: ["§7Click on an armor piece in your", "§7inventory that belongs to this", "§7armor set to donate the full set", "§7to your Museum."] + } + } + }, + rarities: { + display_name: null, + rarity: "special", + tag: { + display: { + Lore: ["§7Click on this item in your", "§7inventory to add it to your", "§7§9Museum§7!"] + } + } + }, + special: null + }, + higher_tier_donated: { + display_name: null, + rarity: "special", + tag: { + display: { + Lore: ["§7Donated as higher tier"] + } + } + } +}; + +export { MUSEUM }; + +setInterval(async () => await retrieveMuseumItems(), 1000 * 60 * 60 * 6); // 6 hours +retrieveMuseumItems(); diff --git a/src/lib/server/constants/update-collections.ts b/src/lib/server/constants/update-collections.ts index 5eff2acfe..076124f48 100644 --- a/src/lib/server/constants/update-collections.ts +++ b/src/lib/server/constants/update-collections.ts @@ -17,7 +17,8 @@ export async function updateCollections() { COLLECTIONS.set(category, collections.collections[category] as Collection); } + + console.log("[COLLECTIONS] Updated collections"); } -updateCollections(); setTimeout(updateCollections, 1000 * 60 * 60 * 12); // 12 hours diff --git a/src/lib/server/constants/update-items.ts b/src/lib/server/constants/update-items.ts index aa3e5ac02..a72fcfe5b 100644 --- a/src/lib/server/constants/update-items.ts +++ b/src/lib/server/constants/update-items.ts @@ -1,5 +1,5 @@ -import { ITEMS } from "$lib/shared/constants/items"; import MONGO from "$lib/server/db/mongo"; +import { ITEMS } from "$lib/shared/constants/items"; import type { DatabaseItem } from "$types/global"; export async function updateItems() { @@ -16,7 +16,8 @@ export async function updateItems() { ITEMS.set(skyBlockItem.skyblock_id, skyBlockItem); } + + console.log("[ITEMS] Updated items"); } -updateItems(); setTimeout(updateItems, 1000 * 60 * 60 * 12); // 12 hours diff --git a/src/lib/server/db/mongo/update-collections.ts b/src/lib/server/db/mongo/update-collections.ts index 37fed3567..4dad9ae3d 100644 --- a/src/lib/server/db/mongo/update-collections.ts +++ b/src/lib/server/db/mongo/update-collections.ts @@ -28,7 +28,7 @@ export async function updateCollections() { const cache = await MONGO.collection("collections").findOne({}); if (cache && cache.lastUpdated > Date.now() - cacheInternal) { - console.log(`[COLLECTIONS] Updated collections in ${(Date.now() - timeNow).toLocaleString()}ms (cached)`); + console.log(`[COLLECTIONS] Fetched collections in ${(Date.now() - timeNow).toLocaleString()}ms (cached)`); return; } @@ -57,7 +57,7 @@ export async function updateCollections() { await MONGO.collection("collections").updateOne({}, { $set: output }, { upsert: true }); - console.log(`[COLLECTIONS] Updated collections in ${(Date.now() - timeNow).toLocaleString()}ms`); + console.log(`[COLLECTIONS] Fetched collections in ${(Date.now() - timeNow).toLocaleString()}ms`); } catch (e) { console.error(e); } diff --git a/src/lib/server/db/mongo/update-items.ts b/src/lib/server/db/mongo/update-items.ts index bc3bd40db..97d608108 100644 --- a/src/lib/server/db/mongo/update-items.ts +++ b/src/lib/server/db/mongo/update-items.ts @@ -9,7 +9,7 @@ export async function updateItems() { const timeNow = Date.now(); const cache = await MONGO.collection("items").findOne({}); if (cache && cache.lastUpdated > Date.now() - cacheInternal) { - console.log(`[ITEMS] Updated items in ${(Date.now() - timeNow).toLocaleString()}ms (cached)`); + console.log(`[ITEMS] Fetched items in ${(Date.now() - timeNow).toLocaleString()}ms (cached)`); return; } @@ -35,7 +35,7 @@ export async function updateItems() { await MONGO.collection("items").updateOne({}, { $set: output }, { upsert: true }); - console.log(`[ITEMS] Updated items in ${(Date.now() - timeNow).toLocaleString()}ms`); + console.log(`[ITEMS] Fetched items in ${(Date.now() - timeNow).toLocaleString()}ms`); } catch (e) { console.error(e); } diff --git a/src/lib/server/helper.ts b/src/lib/server/helper.ts index 2bddd6c80..e8bfd46a0 100644 --- a/src/lib/server/helper.ts +++ b/src/lib/server/helper.ts @@ -1,6 +1,5 @@ import type { Item, ProcessedItem } from "$types/stats"; import { getPrices } from "skyhelper-networth"; -import { v4 } from "uuid"; import { getTexture } from "../custom_resources"; import * as constants from "./constants/constants"; @@ -288,10 +287,7 @@ export function generateUUID() { export function generateItem(data: Partial) { if (!data) { - return { - itemId: v4(), - item_index: Date.now() - } as ProcessedItem; + return {} as ProcessedItem; } const DEFAULT_DATA = { @@ -307,9 +303,7 @@ export function generateItem(data: Partial) { Name: "", Lore: [""] } - }, - itemId: v4(), - item_index: Date.now() + } }; // Making sure rarity is lowercase @@ -349,9 +343,9 @@ export function getHeadTextureUUID(value: string) { return uuid; } +import { STATS_DATA } from "$lib/shared/constants/stats"; import { removeFormatting } from "$lib/shared/helper"; import type { ItemStats } from "$types/processed/profile/stats"; -import { STATS_DATA } from "$lib/shared/constants/stats"; /** * Gets the stats from an item @@ -396,3 +390,20 @@ export function getStatsFromItem(piece: Item): ItemStats { export function capitalizeFirstLetter(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1); } + +/** + * Returns a formatted progress bar string based on the given amount and total. + * + * @param {number} amount - The current amount. + * @param {number} total - The total amount. + * @param {string} [color="a"] - The color of the progress bar. + * @returns {string} The formatted progress bar string. + */ +export function formatProgressBar(amount: number, total: number, completedColor = "a", missingColor = "f") { + const barLength = 25; + const progress = Math.min(1, amount / total); + const progressBars = Math.floor(progress * barLength); + const emptyBars = barLength - progressBars; + + return `${`§${completedColor}§l§m-`.repeat(progressBars)}${`§${missingColor}§l§m-`.repeat(emptyBars)}§r`; +} diff --git a/src/lib/server/helper/NotEnoughUpdates/parseNEURepository.ts b/src/lib/server/helper/NotEnoughUpdates/parseNEURepository.ts index 64137ca80..2cb4032a4 100644 --- a/src/lib/server/helper/NotEnoughUpdates/parseNEURepository.ts +++ b/src/lib/server/helper/NotEnoughUpdates/parseNEURepository.ts @@ -49,7 +49,7 @@ export async function parseNEURepository() { } } - console.log(`Parsed ${items.length.toLocaleString()} items in ${(performance.now() - timeNow).toLocaleString()}ms`); + console.log(`[NOT-ENOUGH-UPDATES] Parsed ${items.length.toLocaleString()} items in ${(performance.now() - timeNow).toLocaleString()}ms`); } parseNEURepository(); diff --git a/src/lib/server/lib.ts b/src/lib/server/lib.ts index 65ef1434d..60df92f31 100644 --- a/src/lib/server/lib.ts +++ b/src/lib/server/lib.ts @@ -154,3 +154,29 @@ export async function fetchPlayer(uuid: string) { return data.player; } + +export async function fetchMuseum(profileId: string) { + const cache = await REDIS.get(`MUSEUM:${profileId}`); + if (cache) { + return JSON.parse(cache); + } + + const response = await fetch(`https://api.hypixel.net/v2/skyblock/museum?profile=${profileId}`, { + headers + }); + + const data = await response.json(); + if (data.success === false) { + throw new SkyCryptError(data?.cause ?? "Request to Hypixel API failed. Please try again!"); + } + + const { members } = data; + if (!members || Object.keys(members).length === 0) { + return null; + } + + // 30 minutes + REDIS.SETEX(`MUSEUM:${profileId}`, 60 * 30, JSON.stringify(members)); + + return members; +} diff --git a/src/lib/server/stats.ts b/src/lib/server/stats.ts index 9f7eb0145..7a60b5dec 100644 --- a/src/lib/server/stats.ts +++ b/src/lib/server/stats.ts @@ -1,12 +1,12 @@ import { REDIS } from "$lib/server/db/redis"; import { getProfiles } from "$lib/server/lib"; import * as stats from "$lib/server/stats/stats"; -import type { Profile, Stats } from "$types/global"; +import type { MuseumRawResponse, Profile, Stats } from "$types/global"; import type { Player } from "$types/raw/player/lib"; const { getAccessories, getPets, getMainStats, getCollections } = stats; -export async function getStats(profile: Profile, player: Player): Promise { +export async function getStats(profile: Profile, player: Player, extra: { museum?: MuseumRawResponse } = {}): Promise { const timeNow = Date.now(); const cache = await REDIS.get(`STATS:${profile.uuid}`); if (cache && process.env.NODE_ENV !== "development") { @@ -15,8 +15,9 @@ export async function getStats(profile: Profile, player: Player): Promise } const userProfile = profile.members[profile.uuid]; + const userMuseum = extra.museum ? extra.museum[profile.uuid] : null; - const items = await stats.getItems(userProfile); + const items = await stats.getItems(userProfile, userMuseum); const [profiles, mainStats, accessories, pets, collections] = await Promise.all([ getProfiles(profile.uuid), getMainStats(userProfile, profile, items), diff --git a/src/lib/server/stats/items.ts b/src/lib/server/stats/items.ts index 805923f9b..548614e59 100644 --- a/src/lib/server/stats/items.ts +++ b/src/lib/server/stats/items.ts @@ -4,10 +4,12 @@ import { getArmor } from "$lib/server/stats/items/armor"; import { getEquipment } from "$lib/server/stats/items/equipment"; import { processItems } from "$lib/server/stats/items/processing"; import { getWardrobe } from "$lib/server/stats/items/wardrobe"; -import type { Items, Member, ProcessedItem } from "$types/global"; +import type { Items, Member, MuseumRaw, ProcessedItem } from "$types/global"; import { getPets, getSkilllTools, getWeapons } from "./items/category"; +import { decodeMusemItems } from "./items/museum"; +import { getMuseumItems } from "./museum"; -export async function getItems(userProfile: Member): Items { +export async function getItems(userProfile: Member, userMuseum: MuseumRaw | null): Items { const INVENTORY = userProfile.inventory; const outputPromises = { @@ -27,7 +29,10 @@ export async function getItems(userProfile: Member): Items { quiver: processItems(INVENTORY?.bag_contents?.quiver?.data ?? "", "quiver", true, []), // BACKPACKS - backpack: {} as Record + backpack: {} as Record, + + // MUSEUM + museumItems: userMuseum ? decodeMusemItems(userMuseum, false, []) : null }; const output = await Promise.all(Object.values(outputPromises)).then((values) => { @@ -88,5 +93,13 @@ export async function getItems(userProfile: Member): Items { output.fishing_tools = getSkilllTools("fishing", allItems); output.pets = getPets(allItems); + const museum = output.museumItems ? await getMuseumItems(output.museumItems) : null; + output.museumItems = [...Object.values(museum?.museumItems.items), ...museum.museumItems.specialItems] + .filter((item) => item.borrowing === false) + .map((item) => item.items) + .flat() + .filter((item) => item !== undefined); + output.museum = museum?.inventory; + return output; } diff --git a/src/lib/server/stats/items/museum.ts b/src/lib/server/stats/items/museum.ts new file mode 100644 index 000000000..57345251a --- /dev/null +++ b/src/lib/server/stats/items/museum.ts @@ -0,0 +1,57 @@ +import * as helper from "$lib/server/helper"; +import type { DecodedMuseumItems } from "$types/global"; +import type { MuseumRaw } from "$types/raw/museum/lib"; +import { processItems } from "./processing"; + +export async function decodeMusemItems(museum: MuseumRaw, customTextures: boolean, packs: string[]): Promise { + const output = { value: 0, items: {}, special: [] } as DecodedMuseumItems; + + const itemPromises = Object.entries(museum.items ?? {}).map(async ([id, data]) => { + const { + donated_time: donatedTime, + borrowing: isBorrowing, + items: { data: decodedData } + } = data; + + const encodedData = await processItems(decodedData, "museum", customTextures, packs); + + if (donatedTime) { + // encodedData.map((i) => helper.addToItemLore(i, ["", `§7Donated: §c`])); + } + + if (isBorrowing) { + encodedData.map((i) => helper.addToItemLore(i, ["", `§7Status: §cBorrowing`])); + } + + return { + id, + value: { + donated_time: donatedTime, + borrowing: isBorrowing ?? false, + items: encodedData.filter((i) => i.id) + } + }; + }); + + const specialPromises = (museum.special ?? []).map(async (special) => { + const { donated_time: donatedTime, items } = special; + const decodedData = await processItems(items.data, "museum", customTextures, packs); + + if (donatedTime) { + // decodedData.map((i) => helper.addToItemLore(i, ["", `§7Donated: §c`])); + } + + return { donated_time: donatedTime, items: decodedData.filter((i) => i.id) }; + }); + + const itemResults = await Promise.all(itemPromises); + itemResults.forEach(({ id, value }) => { + output.items[id] = value; + }); + + output.special = await Promise.all(specialPromises); + + output.value = museum.value; + + return output; +} diff --git a/src/lib/server/stats/items/processing.ts b/src/lib/server/stats/items/processing.ts index 1005e547b..f5482186e 100644 --- a/src/lib/server/stats/items/processing.ts +++ b/src/lib/server/stats/items/processing.ts @@ -7,11 +7,11 @@ import minecraftData from "minecraft-data"; const mcData = minecraftData("1.8.9"); import { getUsername } from "$lib/server/lib"; +import { formatNumber } from "$lib/shared/helper"; import type { StatsData } from "$types/processed/profile/stats"; import type { GemTier, Gemstone, Item, ProcessedItem } from "$types/stats"; import nbt, { parse } from "prismarine-nbt"; import { v4 } from "uuid"; -import { formatNumber } from "$lib/shared/helper"; import { STATS_DATA } from "$lib/shared/constants/stats"; export function itemSorter(a: ProcessedItem, b: ProcessedItem) { @@ -90,7 +90,7 @@ function getCategories(type: string, item: Item) { export function parseItemGems(gems: { [key: string]: string }, rarity: string) { const slots = { normal: Object.keys(constants.GEMSTONES), - special: ["UNIVERSAL", "COMBAT", "OFFENSIVE", "DEFENSIVE", "MINING"], + special: ["UNIVERSAL", "COMBAT", "OFFENSIVE", "DEFENSIVE", "MINING", "CHISEL"], ignore: ["unlocked_slots"] }; diff --git a/src/lib/server/stats/main_stats.ts b/src/lib/server/stats/main_stats.ts index 8f0ea142c..867a5b629 100644 --- a/src/lib/server/stats/main_stats.ts +++ b/src/lib/server/stats/main_stats.ts @@ -31,10 +31,12 @@ export async function getMainStats(userProfile: Member, profile: Profile, items: .flat() : [], fishing_bag: items.fishing_bag ?? [], - potion_bag: items.potion_bag ?? [] + potion_bag: items.potion_bag ?? [], + museum: items.museumItems ?? [] }; const predecodedNetworth = await getPreDecodedNetworth(userProfile, networthItems, bank, networthOptions); + items.museumItems = []; return { joined: userProfile.profile?.first_join ?? 0, diff --git a/src/lib/server/stats/museum.ts b/src/lib/server/stats/museum.ts new file mode 100644 index 000000000..3dea67d63 --- /dev/null +++ b/src/lib/server/stats/museum.ts @@ -0,0 +1,218 @@ +import { MUSEUM, MUSEUM_INVENTORY } from "$constants/museum"; +import * as helper from "$lib/server/helper"; +import { titleCase } from "$lib/shared/helper"; +import type { DecodedMuseumItems, MuseumItems, ProcessedItem } from "$types/global"; +import type { MuseumItem } from "$types/raw/museum/lib"; + +function markChildrenAsDonated(children: string, output: MuseumItems, decodedMuseum: DecodedMuseumItems) { + output[children] = { + // if item data exists in decodedMuseum, use it, otherwise mark it as donated as child + ...(decodedMuseum.items[children] ? decodedMuseum.items[children] : { donated_as_child: true }), + id: children + }; + + const childOfChild = MUSEUM.children[children]; + if (childOfChild !== undefined) { + markChildrenAsDonated(childOfChild, output, decodedMuseum); + } +} + +async function processMuseumItems(decodedMuseum: DecodedMuseumItems) { + const output = {} as MuseumItems; + for (const item of MUSEUM.getAllItems()) { + const itemData = decodedMuseum.items[item]; + if (itemData === undefined && output[item] === undefined) { + output[item] = { + missing: true, + id: item + }; + continue; + } + + const children = MUSEUM.children[item]; + if (children !== undefined) { + markChildrenAsDonated(item, output, decodedMuseum); + } + + if (itemData !== undefined) { + output[item] = { + ...itemData, + id: item + }; + } + } + + const getCategoryItems = (category: keyof typeof MUSEUM) => Object.keys(output).filter((i) => (MUSEUM[category] as string[]).includes(i)); + const getMissingItems = (category: keyof typeof MUSEUM) => getCategoryItems(category).filter((i) => output[i].missing === true); + const getMaxMissingItems = (category: keyof typeof MUSEUM) => getMissingItems(category).filter((i) => Object.values(MUSEUM.children).includes(i) === false); + + const weapons = getCategoryItems("weapons"); + const armor = getCategoryItems("armor"); + const rarities = getCategoryItems("rarities"); + + return { + value: decodedMuseum.value ?? 0, + appraisal: false, + total: { + amount: Object.keys(output).filter((i) => !output[i].missing).length, + total: Object.keys(output).length + }, + weapons: { + amount: weapons.filter((i) => !output[i].missing).length, + total: weapons.length + }, + armor: { + amount: armor.filter((i) => !output[i].missing).length, + total: armor.length + }, + rarities: { + amount: rarities.filter((i) => !output[i].missing).length, + total: rarities.length + }, + special: { + amount: decodedMuseum.special.length + }, + items: output, + specialItems: decodedMuseum.special, + missing: { + main: ["weapons", "armor", "rarities"].map((c) => getMissingItems(c as keyof typeof MUSEUM)).flat(), + max: ["weapons", "armor", "rarities"].map((c) => getMaxMissingItems(c as keyof typeof MUSEUM)).flat() + } + }; +} + +function formatMuseumItemProgress( + presetItem: ProcessedItem & { + progressType: string; + inventoryType: string; + }, + museum: MuseumItems +) { + if (presetItem.progressType === undefined) { + return presetItem; + } + + if (presetItem.progressType === "appraisal") { + const { appraisal, value } = museum; + + helper.addToItemLore(presetItem, [`§7Museum Appraisal Unlocked: ${appraisal ? "§aYes" : "§cNo"}`, "", `§7Museum Value: §6${Math.floor(value).toLocaleString()} Coins §7(§6${helper.formatNumber(value)}§7)`]); + return presetItem; + } + + if (presetItem.progressType === "special") { + const { amount } = museum[presetItem.progressType]; + helper.addToItemLore(presetItem, [`§7Items Donated: §b${amount}`, "", "§eClick to view!"]); + return presetItem; + } + + const { amount, total } = museum[presetItem.progressType]; + helper.addToItemLore(presetItem, [`§7Items Donated: §e${Math.floor((amount / total) * 100)}§6%`, `§9§l${helper.formatProgressBar(amount, total, "9")} §b${amount} §9/ §b${total}`, "", "§eClick to view!"]); + + return presetItem; +} + +export async function getMuseumItems(decodedMuseumItems: DecodedMuseumItems) { + const museumData = await processMuseumItems(decodedMuseumItems); + + const output = []; + for (let i = 0; i < 6 * 9; i++) { + output.push(helper.generateItem({ id: undefined })); + } + + for (const item of MUSEUM_INVENTORY.inventory) { + const itemSlot = formatMuseumItemProgress(JSON.parse(JSON.stringify(item)), museumData) as MuseumItem; + + const inventoryType = item.inventoryType as keyof typeof museumData; + if (inventoryType === undefined) { + if (itemSlot && itemSlot.display_name) { + output[item.position] = itemSlot; + } + + continue; + } + + const museumItems = typeof museumData[inventoryType] === "object" && "total" in museumData[inventoryType] ? museumData[inventoryType].total : typeof museumData[inventoryType] === "object" && "amount" in museumData[inventoryType] ? museumData[inventoryType].amount : undefined; + const pages = Math.ceil(museumItems / MUSEUM_INVENTORY.item_slots.length); + + for (let page = 0; page < pages; page++) { + // FRAME + for (let i = 0; i < 6 * 9; i++) { + if (itemSlot.containsItems[i]) { + const presetItem = JSON.parse(JSON.stringify(itemSlot.containsItems[i])); + const formattedItem = formatMuseumItemProgress(presetItem, museumData); + if (formattedItem === undefined) { + continue; + } + + itemSlot.containsItems[i + page * 54] = formattedItem; + } + + itemSlot.containsItems[i + page * 54] ??= helper.generateItem({ id: undefined }); + } + + // CLEAR FIRST 4 ITEMS + for (let i = 0; i < 4; i++) { + itemSlot.containsItems[i + page * 54] = helper.generateItem({ id: undefined }); + } + + // CATEGORIES + for (const [index, slot] of Object.entries(MUSEUM_INVENTORY.item_slots)) { + const slotIndex = parseInt(index) + page * MUSEUM_INVENTORY.item_slots.length; + + // SPECIAL ITEMS CATEGORY + if (inventoryType === "special") { + const museumItem = museumData.specialItems[slotIndex]; + if (museumItem === undefined) { + continue; + } + + const itemData = museumItem.items[0]; + + itemSlot.containsItems[slot + page * 54] = helper.generateItem(itemData); + continue; + } + + // WEAPONS, ARMOR & RARITIES + const itemArray = MUSEUM_INVENTORY[inventoryType as keyof typeof MUSEUM_INVENTORY]; + const itemId = Array.isArray(itemArray) ? (itemArray[slotIndex] as unknown as string) : undefined; + if (itemId === undefined) { + continue; + } + + const museumItem = museumData.items[itemId]; + // MISSING ITEM + if (museumItem === undefined) { + const itemData = JSON.parse(JSON.stringify(MUSEUM_INVENTORY.missing_item[inventoryType as "weapons" | "armor" | "rarities"])); + itemData.display_name = helper.titleCase(MUSEUM.armor_to_id[itemId] ?? itemId); + + itemSlot.containsItems[slot + page * 54] = helper.generateItem(itemData); + continue; + } + + // DONATED HIGHER TIER + if (museumItem.donated_as_child) { + const itemData = JSON.parse(JSON.stringify(MUSEUM_INVENTORY.higher_tier_donated)); + itemData.display_name = titleCase(MUSEUM.armor_to_id[itemId] ?? itemId); + + itemSlot.containsItems[slot + page * 54] = helper.generateItem(itemData); + continue; + } + + // NORMAL ITEM + const itemData = museumItem.data[0]; + if (museumItem.data.length > 1) { + itemData.containsItems = museumItem.data.map((i: ProcessedItem) => helper.generateItem(i)); + } + + itemSlot.containsItems[slot + page * 54] = helper.generateItem(itemData); + } + } + + output[item.position] = itemSlot; + } + + return { + museumItems: museumData, + inventory: output + }; +} diff --git a/src/lib/types/global.d.ts b/src/lib/types/global.d.ts index 8bddf20e2..e53d2c8c3 100644 --- a/src/lib/types/global.d.ts +++ b/src/lib/types/global.d.ts @@ -1,2 +1,3 @@ +export * from "./raw/museum/lib"; export * from "./raw/profile/lib"; export * from "./stats"; diff --git a/src/lib/types/helper.d.ts b/src/lib/types/helper.d.ts index b3beed76c..5c449c941 100644 --- a/src/lib/types/helper.d.ts +++ b/src/lib/types/helper.d.ts @@ -1,2 +1,12 @@ type ColorCode = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f"; type FormatCode = "k" | "l" | "m" | "n" | "o"; + +type MuseumConstants = { + armor_to_id: Record; + armor_sets: Record; + children: Record; + weapons: string[]; + armor: string[]; + rarities: string[]; + getAllItems: () => string[]; +}; diff --git a/src/lib/types/processed/profile/items.d.ts b/src/lib/types/processed/profile/items.d.ts index 3d7927f1a..8f79a2198 100644 --- a/src/lib/types/processed/profile/items.d.ts +++ b/src/lib/types/processed/profile/items.d.ts @@ -40,6 +40,13 @@ export type DatabaseItem = { skyblock_id?: string; color?: string; damage?: number; + museum_data?: { + armor_set_donation_xp: number; + donation_xp: number; + type: string; + parent: Record; + game_stage: string; + }; }; export type ItemQuery = { @@ -125,7 +132,6 @@ export type ProcessedItem = { damage?: number; glowing?: boolean; position?: number; - itemId: string; item_index: number; }; @@ -184,6 +190,8 @@ export type Items = { potion_bag: ProcessedItem[]; quiver: ProcessedItem[]; // candy_inventory: ProcessedItem[]; + museumItems: ProcessedItem[]; + museum: ProcessedItem[]; }; export type SpecialAccessory = { diff --git a/src/lib/types/raw/museum/lib.d.ts b/src/lib/types/raw/museum/lib.d.ts new file mode 100644 index 000000000..3b129750e --- /dev/null +++ b/src/lib/types/raw/museum/lib.d.ts @@ -0,0 +1,34 @@ +import type { ProcessedItem } from "$types/global"; + +export type MuseumRawResponse = { + [key: string]: MuseumRaw; +}; + +export type MuseumRaw = { + value: number; + appraised: boolean; + items: Record< + string, + { + donated_time: number; + featured_slot: string; + borrowing: boolean; + items: { + type: number; + data: string; + }; + } + >; + special: { + donated_time: number; + items: { + type: number; + data: string; + }; + }[]; +}; + +type MuseumItem = ProcessedItem & { + position: number; + containsItems: ProcessedItem[]; +}; diff --git a/src/lib/types/raw/profile/lib.d.ts b/src/lib/types/raw/profile/lib.d.ts index b9bbd193e..beea313ee 100644 --- a/src/lib/types/raw/profile/lib.d.ts +++ b/src/lib/types/raw/profile/lib.d.ts @@ -428,3 +428,16 @@ export type ExperimentationGame = { bonus_clicks: number; [string: string]: number; }; + +export type DecodedMuseumItems = { + value: number; + items: Record; + special: { donated_time: number; items: ProcessedItem[] }[]; +}; + +export type MuseumItems = { + [key: string]: ProcessedItem & { + donated_as_child: boolean; + id: string; + }; +}; diff --git a/src/routes/api/museum/[paramProfile]/+server.ts b/src/routes/api/museum/[paramProfile]/+server.ts new file mode 100644 index 000000000..3b352d609 --- /dev/null +++ b/src/routes/api/museum/[paramProfile]/+server.ts @@ -0,0 +1,15 @@ +import { fetchMuseum } from "$lib/server/lib"; +import { decodeMusemItems } from "$lib/server/stats/items/museum"; +import { getMuseumItems } from "$lib/server/stats/museum"; +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ params }) => { + const { paramProfile } = params; + + const museum = await fetchMuseum(paramProfile); + const decodedMuseum = await decodeMusemItems(museum["fb3d96498a5b4d5b91b763db14b195ad"], false, []); + const processedMuseum = await getMuseumItems(decodedMuseum); + + return json(processedMuseum); +}; diff --git a/src/routes/api/stats/[paramPlayer=player]/+server.ts b/src/routes/api/stats/[paramPlayer=player]/+server.ts index 2763c6757..90da254c5 100644 --- a/src/routes/api/stats/[paramPlayer=player]/+server.ts +++ b/src/routes/api/stats/[paramPlayer=player]/+server.ts @@ -1,4 +1,4 @@ -import { fetchPlayer, getProfile } from "$lib/server/lib"; +import { fetchMuseum, fetchPlayer, getProfile } from "$lib/server/lib"; import { getStats } from "$lib/server/stats"; import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; @@ -8,8 +8,9 @@ export const GET: RequestHandler = async ({ params }) => { const { paramPlayer } = params; const [profile, player] = await Promise.all([getProfile(paramPlayer, null), fetchPlayer(paramPlayer)]); + const museum = await fetchMuseum(profile.profile_id); - const stats = await getStats(profile, player); + const stats = await getStats(profile, player, { museum }); console.log(`/api/stats/${paramPlayer} took ${Date.now() - timeNow}ms`); return json(stats); diff --git a/src/routes/api/stats/[paramPlayer=player]/[paramProfile]/+server.ts b/src/routes/api/stats/[paramPlayer=player]/[paramProfile]/+server.ts index d374827ca..5aceb902d 100644 --- a/src/routes/api/stats/[paramPlayer=player]/[paramProfile]/+server.ts +++ b/src/routes/api/stats/[paramPlayer=player]/[paramProfile]/+server.ts @@ -1,4 +1,4 @@ -import { fetchPlayer, getProfile } from "$lib/server/lib"; +import { fetchMuseum, fetchPlayer, getProfile } from "$lib/server/lib"; import { getStats } from "$lib/server/stats"; import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; @@ -8,8 +8,9 @@ export const GET: RequestHandler = async ({ params }) => { const { paramPlayer, paramProfile } = params; const [profile, player] = await Promise.all([getProfile(paramPlayer, paramProfile), fetchPlayer(paramPlayer)]); + const museum = await fetchMuseum(profile.profile_id); - const stats = await getStats(profile, player); + const stats = await getStats(profile, player, { museum }); console.log(`/api/stats/${paramPlayer} took ${Date.now() - timeNow}ms`); return json(stats);