diff --git a/.nvmrc b/.nvmrc index 37e391feb..95c758cad 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.17.0 +v18.12.1 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 0daf16a13..1d5c0dc0b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -48,6 +48,15 @@ "cwd": "${workspaceFolder}", "runtimeArgs": ["verify-server", "start"], "outputCapture": "std" + }, + { + "type": "node", + "request": "launch", + "name": "Prod API", + "runtimeExecutable": "yarn", + "cwd": "${workspaceFolder}", + "runtimeArgs": ["api", "prod-start"], + "outputCapture": "std" } ] } diff --git a/packages/hypixel-api-client/package.json b/packages/hypixel-api-client/package.json new file mode 100644 index 000000000..88fdc0861 --- /dev/null +++ b/packages/hypixel-api-client/package.json @@ -0,0 +1,14 @@ +{ + "name": "@statsify/hypixel-api-client", + "version": "0.0.0", + "main": "dist/index.js", + "types": "src/index.ts", + "scripts": { + "build": "swc src --config-file ../../.swcrc --out-dir dist", + "test:types": "tsc --noEmit", + "lint": "TIMING=1 eslint './{src,tests}/**/*.{ts,tsx,js,jsx}' --fix" + }, + "dependencies": { + "@statsify/util": "workspace:^" + } +} diff --git a/packages/hypixel-api-client/src/index.ts b/packages/hypixel-api-client/src/index.ts new file mode 100644 index 000000000..137975508 --- /dev/null +++ b/packages/hypixel-api-client/src/index.ts @@ -0,0 +1,240 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { HypixelPlayerResponse } from "./types"; +import { setTimeout } from "node:timers/promises"; + +export class HypixelAPIError extends Error { + public static BadRequest = new HypixelAPIError("Bad Request"); + public static InvalidAPIKey = new HypixelAPIError("Invalid API Key"); + public static NotFound = new HypixelAPIError("Not Found"); + public static InvalidData = new HypixelAPIError("Invalid Data"); + public static RatelimitExceeded = new HypixelAPIError("Ratelimit Exceeded"); + public static InternalServerError = new HypixelAPIError("Internal Server Error"); + public static DataUnavailable = new HypixelAPIError("Data Unavailable"); + public static UnknownError = new HypixelAPIError("Unknown Error"); + public static RequestTimeout = new HypixelAPIError("Request Timeout"); + + public constructor(message: string) { + super(message); + this.name = HypixelAPIError.name; + } + + public static fromCode(code: number) { + switch (code) { + case 400: { + return HypixelAPIError.BadRequest; + } + case 403: { + return HypixelAPIError.InvalidAPIKey; + } + case 404: { + return HypixelAPIError.NotFound; + } + case 422: { + return HypixelAPIError.InvalidData; + } + case 429: { + return HypixelAPIError.RatelimitExceeded; + } + case 500: { + return HypixelAPIError.InternalServerError; + } + case 503: { + return HypixelAPIError.DataUnavailable; + } + default: { + return HypixelAPIError.UnknownError; + } + } + } +} + +export interface HypixelAPIConfig { + key: string; + autoSleep: boolean; + timeout?: number; +} + +/** + * ID - Guild ID + * Player - Player UUID + * Name - Guild name + */ +export type GuildQuery = { id: string } | { player: string } | { name: string }; + +/** + * UUID - Auction UUID + * Player - Player UUID + * Profile - Profile UUID + */ +export type SkyblockAuctionQuery = + | { uuid: string } + | { player: string } + | { profile: string }; + +export type Resource = + | "games" + | "achievements" + | "challenges" + | "quests" + | "guilds/achievements" + | "vanity/pets" + | "vanity/companions" + | "skyblock/collections" + | "skyblock/skills" + | "skyblock/items" + | "skyblock/election" + | "skyblock/bingo"; + +export class HypixelAPI { + public limit: number; + public remaining: number; + public retryAfter?: number; + + public constructor(private config: HypixelAPIConfig) { + this.limit = 120; + this.remaining = this.limit; + } + + public player(uuid: string): Promise { + return this.get("player", { uuid }); + } + + public friends(uuid: string) { + return this.get("friends", { uuid }); + } + + public recentgames(uuid: string) { + return this.get("recentgames", { uuid }); + } + + public status(uuid: string) { + return this.get("status", { uuid }); + } + + public guild(params: GuildQuery) { + return this.get("guild", params); + } + + public boosters() { + return this.get("boosters"); + } + + public gamecounts() { + return this.get("counts"); + } + + public leaderboards() { + return this.get("leaderboards"); + } + + public watchdog() { + return this.get("punishmentstats"); + } + + public skyblockNews() { + return this.get("skyblock/news"); + } + + public skyblockAuction(params: SkyblockAuctionQuery) { + return this.get("skyblock/auction", params); + } + + public skyblockAuctions(page: number) { + return this.get("skyblock/auctions", { page: `${page}` }); + } + + public skyblockAuctionsEnded() { + return this.get("skyblock/auctions_ended"); + } + + public skyblockBazaar() { + return this.get("skyblock/bazaar"); + } + + public skyblockProfile(profile: string) { + return this.get("skyblock/profile", { profile }); + } + + public skyblockProfiles(uuid: string) { + return this.get("skyblock/profiles", { uuid }); + } + + public skyblockBingo(uuid: string) { + return this.get("skyblock/bingo", { uuid }); + } + + public skyblockFiresales() { + return this.get("skyblock/firesales"); + } + + public async resource(resource: Resource) { + return this.get(`resources/${resource}`); + } + + /** + * + * @param endpoint The endpoint to fetch from https://api.hypixel.net/ + * @param params The query parameters to send + * @returns The API response + * @throws HypixelAPIError + */ + private async get( + endpoint: string, + params: Record = {} + ): Promise { + const controller = new AbortController(); + + const url = new URL( + `https://api.hypixel.net${endpoint}?${new URLSearchParams(params)}` + ); + + try { + if (this.config.timeout) { + setTimeout(this.config.timeout, controller.abort.bind(controller)); + } + + const response = await fetch(url, { + headers: { "API-KEY": this.config.key }, + method: "GET", + signal: controller.signal, + }); + + const headers = response.headers; + const currentLimit = headers.get("rateLimit-limit"); + const currentRemaining = headers.get("rateLimit-remaining"); + const currentRetryAfter = headers.get("retry-after"); + + this.limit = currentLimit ? +currentLimit : this.limit; + this.remaining = currentRemaining ? +currentRemaining : this.remaining - 1; + + if (currentRetryAfter) this.retryAfter = +currentRetryAfter * 1000; + if (!response.ok) throw HypixelAPIError.fromCode(response.status); + + this.retryAfter = undefined; + + const data = await response.json(); + return data; + } catch (error: any) { + if (error === HypixelAPIError.RatelimitExceeded && this.config.autoSleep) { + const sleep = this.retryAfter ?? 60_000; + await setTimeout(sleep); + return this.get(endpoint, params); + } + + this.retryAfter = undefined; + + if (error.name === "AbortError") throw HypixelAPIError.RequestTimeout; + + throw error; + } + } +} + +export * from "./types"; diff --git a/packages/hypixel-api-client/src/types/base-hypixel-response.d.ts b/packages/hypixel-api-client/src/types/base-hypixel-response.d.ts new file mode 100644 index 000000000..d375f7e45 --- /dev/null +++ b/packages/hypixel-api-client/src/types/base-hypixel-response.d.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +export interface BaseHypixelResponse { + success: boolean; +} diff --git a/packages/hypixel-api-client/src/types/games.d.ts b/packages/hypixel-api-client/src/types/games.d.ts new file mode 100644 index 000000000..5485ce7d9 --- /dev/null +++ b/packages/hypixel-api-client/src/types/games.d.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +export type HypixelLobby = + | "Adventure" + | "ArcadeGames" + | "Arena" + | "Bedwars" + | "BlitzLobby" + | "BuildBattle" + | "CopsnCrims" + | "Duels" + | "Housing" + | "Legacy" + | "MainLobby" + | "MegaWalls" + | "MurderMystery" + | "NewMainLobby" + | "Paintball" + | "Prototype" + | "QuakeCraft" + | "SC" + | "SkyClash" + | "Skywars" + | "SkywarsAug2017" + | "SkywarsChristmas2017" + | "SkywarsStandard2022" + | "SpeedUHC" + | "SuperSmash" + | "TNT" + | "TheWallsLobby" + | "Tourney" + | "TruePVPParkour" + | "Turbo" + | "Warlords" + | "WoolGames" + | "blitz" + | "bossfight" + | "christmas" + | "main" + | "mainLobby2017" + | "mainLobby2022" + | "megawallslobby" + | "newwalls" + | "overhaul" + | "quakelobby" + | "uhc" + | "vampirez"; + +export type HypixelGameMode = + | "Arcade" + | "Arena" + | "Battleground" + | "Bedwars" + | "BuildBattle" + | "Duels" + | "GingerBread" + | "Housing" + | "HungerGames" + | "Legacy" + | "MCGO" + | "MurderMystery" + | "Paintball" + | "Pit" + | "Quake" + | "SkyClash" + | "SkyWars" + | "SpeedUHC" + | "SuperSmash" + | "TNTGames" + | "TrueCombat" + | "UHC" + | "VampireZ" + | "Walls" + | "Walls3" + | "WoolGames"; + +export type HypixelAchievementMode = + | "arcade" + | "arena" + | "bedwars" + | "blitz" + | "buildbattle" + | "christmas2017" + | "copsandcrims" + | "duels" + | "easter" + | "general" + | "gingerbread" + | "halloween2017" + | "housing" + | "murdermystery" + | "paintball" + | "pit" + | "quake" + | "skyblock" + | "skyclash" + | "skywars" + | "speeduhc" + | "summer" + | "supersmash" + | "tntgames" + | "truecombat" + | "uhc" + | "vampirez" + | "walls" + | "walls3" + | "warlords" + | "woolgames"; diff --git a/packages/hypixel-api-client/src/types/helpers.d.ts b/packages/hypixel-api-client/src/types/helpers.d.ts new file mode 100644 index 000000000..2871f0a88 --- /dev/null +++ b/packages/hypixel-api-client/src/types/helpers.d.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +type PrependNextNum> = A["length"] extends infer T + ? ((t: T, ...a: A) => void) extends (...x: infer X) => void + ? X + : never + : never; + +type EnumerateInternal, N extends number> = { + 0: A; + 1: EnumerateInternal, N>; +}[N extends A["length"] ? 0 : 1]; + +export type Enumerate = EnumerateInternal<[], N> extends (infer E)[] + ? E + : never; + +export type IntRange = Exclude< + Enumerate, + Enumerate +>; + +export interface BasicStats { + [key: string]: any; + coins?: number; + lastTourneyAd?: number; +} + +export interface NBTData { + type: number; + data: ArrayBuffer; +} + +export type APIData = Record; diff --git a/packages/hypixel-api-client/src/types/index.ts b/packages/hypixel-api-client/src/types/index.ts new file mode 100644 index 000000000..485f14ebe --- /dev/null +++ b/packages/hypixel-api-client/src/types/index.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +export * from "./base-hypixel-response"; +export * from "./player"; +export * from "./player-stats"; +export * from "./stats"; +export * from "./helpers"; diff --git a/packages/hypixel-api-client/src/types/player-stats.d.ts b/packages/hypixel-api-client/src/types/player-stats.d.ts new file mode 100644 index 000000000..8258ecb52 --- /dev/null +++ b/packages/hypixel-api-client/src/types/player-stats.d.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import type { APIData, BasicStats } from "./helpers"; +import type { HypixelGameMode } from "./games"; +import type { HypixelPitStats } from "./stats"; + +export type HypixelPlayerStats = { + [key: HypixelGameMode]: BasicStats | APIData; + Pit: HypixelPitStats; +}; diff --git a/packages/hypixel-api-client/src/types/player.d.ts b/packages/hypixel-api-client/src/types/player.d.ts new file mode 100644 index 000000000..2323997f4 --- /dev/null +++ b/packages/hypixel-api-client/src/types/player.d.ts @@ -0,0 +1,121 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { BaseHypixelResponse } from "./base-hypixel-response"; +import { HypixelAchievementMode, HypixelGameMode, HypixelLobby } from "./games"; +import { HypixelNormalRank } from "./ranks"; +import { HypixelPlayerStats } from "./player-stats"; + +export type ChallengesTime = `day_${string}` | "all__time"; +export type AchievementName = `${HypixelAchievementMode}_${string}`; + +export interface PeriodicChallenges { + [key: `${Uppercase}_${string}`]: number; +} + +export type Challenges = { + [key: ChallengesTime]: PeriodicChallenges; + all_time?: PeriodicChallenges; +}; + +export interface Quest { + completions?: { time: number }[]; + active?: { + started?: number; + objectives?: { [key: string]: number }; + }; +} + +export interface PlayerSettings { + fishCollectorShowCaught?: boolean; +} + +export interface SeasonalStats { + [key: "halloween" | "summer"]: { "2022"?: { levelling?: { experience: number } } }; + silver: number; +} + +export type AchievementRewardsNew = { + [key: `for_points_${number}00`]: number; +}; + +export interface SocialMedia { + links: { + DISCORD?: string; + HYPIXEL?: string; + INSTAGRAM?: string; + MIXER?: string; + TWITCH?: string; + TWITTER?: string; + YOUTUBE?: string; + }; +} + +export type ParkourCompletions = { + [key: HypixelLobby]: { timeStart?: number; timeTook?: number }[]; +}; + +export interface HypixelPlayer { + [key: string]: any; + + /** + * The mongo ID of the player in hypixel + */ + _id?: string; + + _settings?: PlayerSettings; + + /** + * The players UUID + */ + uuid: string; + + /** + * The timestamp of the players first login to hypixel + */ + firstLogin?: number; + + /** + * The name of the player all lowercase + */ + playername?: Lowercase; + + /** + * The name of the player + */ + displayname: PName; + + achievementsOneTime?: AchievementName[]; + achievementRewardsNew?: AchievementRewardsNew; + networkExp?: number; + karma?: number; + stats?: HypixelPlayerStats; + socialMedia?: SocialMedia; + achievements?: Record; + newPackageRank?: HypixelNormalRank; + totalRewards?: number; + totalDailyRewards?: number; + rewardStreak?: number; + rewardScore?: number; + rewardHighScore?: number; + achievementPoints?: number; + battlePassGlowStatus?: boolean; + firstLogin?: number; + seasonal?: SeasonalStats; + challenges?: Challenges; + quests?: Record; + parkourCheckpointBests?: { [key: HypixelLobby]: number[] }; + parkourCompletions?: ParkourCompletions; + rankPlusColor?: string; + monthlyPackageRank?: "NONE" | "SUPERSTAR"; + monthlyRankColor?: "GOLD" | "AQUA"; +} + +export interface HypixelPlayerResponse extends BaseHypixelResponse { + player: HypixelPlayer; +} diff --git a/packages/hypixel-api-client/src/types/ranks.d.ts b/packages/hypixel-api-client/src/types/ranks.d.ts new file mode 100644 index 000000000..8e431ed39 --- /dev/null +++ b/packages/hypixel-api-client/src/types/ranks.d.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +export enum HypixelNormalRank { + "NONE", + "NORMAL", + "VIP", + "VIP_PLUS", + "MVP", + "MVP_PLUS", +} + +export enum HypixelSpecialRanks { + "YOUTUBER", + "ADMIN", + "GAME_MASTER", + "OWNER", +} + +export type HypixelRanks = HypixelNormalRank | HypixelSpecialRanks; diff --git a/packages/hypixel-api-client/src/types/stats/index.ts b/packages/hypixel-api-client/src/types/stats/index.ts new file mode 100644 index 000000000..a14f8606a --- /dev/null +++ b/packages/hypixel-api-client/src/types/stats/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +export * from "./pit-stats"; diff --git a/packages/hypixel-api-client/src/types/stats/pit-stats.d.ts b/packages/hypixel-api-client/src/types/stats/pit-stats.d.ts new file mode 100644 index 000000000..897af1cb0 --- /dev/null +++ b/packages/hypixel-api-client/src/types/stats/pit-stats.d.ts @@ -0,0 +1,654 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { BasicStats, IntRange, NBTData } from "../helpers"; + +export enum PitGenesisFactions { + angel = "angel", + demon = "demon", +} + +export interface PitContract { + /** + * either EASY or HARD, representing a Novice or Big Time contract respectively. + */ + difficulty: "EASY" | "HARD"; + + /** + * Amount of gold the player earned from completing the contract. + */ + gold_reward: number; + + /** + * Object. Contains a key representing the objective the player needs to complete for the contract. + */ + requirements: Record; + + /** + * Object. Contains a key representing the progress the player has already made in the contract. + */ + progress: Record; + + /** + * The number of Chunks of Vile the player will receive if they complete the contract. + */ + chunk_of_viles_reward: number; + + completion_date: 0; + + /** + * How many ticks are left in the contract's 5-minute timer. One tick is equal to 0.05 seconds. + */ + remaining_ticks: number; + + /** + * Description of the contract's objective. + */ + key: string; +} + +export interface PitBounty { + /** + * The gold amount of the bounty bump + */ + amount: number; + + /** + * @deprecated Does not count down and is always an unreasonably high number + */ + remainingTicks: number; + + /** + * The Unix timestamp of when the player received the bounty + */ + timestamp: number; +} + +interface PitShopsThrottle { + [key: `${"buyer_" | ""}${"Hay" | "Bread" | "Fish"}`]: { + /** + * The latest date the player sold items to that NPC. + */ + lastEpochDay: number; + + /** + * The number of items that were sold on `lastEpochDay`. + */ + count: number; + }; +} + +export interface PitStatsPTL { + assists: number; + cash_earned: number; + damage_dealt: number; + damage_received: number; + deaths: number; + gapple_eaten: number; + joins: number; + jumped_into_pit: number; + kills: number; + left_clicks: number; + max_streak: number; + melee_damage_dealt: number; + melee_damage_received: number; + playtime_minutes: number; + sword_hits: number; + arrow_hits: number; + arrows_fired: number; + bow_damage_dealt: number; + bow_damage_received: number; + ghead_eaten: number; + launched_by_launchers: number; + contracts_completed: number; + contracts_started: number; + fishing_rod_launched: number; + chat_messages: number; + diamond_items_purchased: number; + enderchest_opened: number; + fished_anything: number; + wheat_farmed: number; + sewer_treasures_found: number; + blocks_placed: number; + blocks_broken: number; + king_quest_completion: number; + lava_bucket_emptied: number; + + /** + * Gold earned from picking up ingots. + */ + ingots_cash: number; + + /** + * Total number of gold ingots picked up. + */ + ingots_picked_up: number; + + /** + * Total amount of HP healed with the Vampire perk. + */ + vampire_healed_hp: number; + + /** + * Total number of Rage Potatoes eaten from the Rage Pit event. + */ + rage_potatoes_eaten: number; + + enchanted_tier1: number; + enchanted_tier2: number; + enchanted_tier3: number; + + soups_drank: number; + night_quests_completed: number; + obsidian_broken: number; + + /** + * Number of diamond armor pieces obtained from the Lucky Diamond perk. + */ + lucky_diamond_pieces: number; + + /** + * Number of arrows earned from the Spammer perk. + */ + endless_quiver_arrows: number; + + /** + * Amount of gold earned from the Trickle Down perk. + */ + extra_from_trickle_down: number; + + /** + * Number of bounties worth 500g or greater claimed with the Bounty Hunter perk. + */ + bounties_of_500g_with_bh: number; + + fishes_fished: number; + rambo_kills: number; + launched_by_angel_spawn: number; + launched_by_demon_spawn: number; + rage_pants_crafted: number; + gold_from_farming: number; + dark_pants_crated: number; + dark_pants_t2: number; + hidden_jewel_triggers: number; + gold_from_selling_fish: number; +} + +export interface PitOutgoingOffer { + /** + * Unix timestamp of when the command was sent. + */ + issued_ms: number; + + /** + * The price, in gold, the recipient will need to pay to receive the item. + */ + gold: number; + + /** + * Object. Contains a byte array of the object being sent. + */ + item: NBTData; + + /** + * Unknown use; seemingly always −1, regardless whether the item is pants or not. + */ + pants: -1; + + /** + * UUID of the trade. Can be used in the /inspectoffer [uuid] command to open the offer GUI without needing to click. + */ + uuid: string; + + /** + * Unknown use; seemingly always 0, regardless of how many pants are included in the offer. + */ + pants_count: 0; + + /** + * UUID of the recipient. + */ + target: string; +} + +export interface PitKingsQuest { + kills: number; + renown: number; + last_completed: number; + last_accepted: number; +} + +export interface PitUnlocks { + tier: number; + acquireDate: number; + key: string; +} + +export interface PitPrestiges { + index: IntRange<1, 50>; + xp_on_prestige: number; + timestamp: number; +} + +export interface PitRenownUnlocks { + tier: number; + acquireDate: number; + key: string; +} + +export enum PitChatOptions { + "kill_feed", + "prestige_announcements", + "minor_events", + "streaks", + "player_chat", + "bounties", + "misc", +} + +export interface PitProfile { + // + // All index types are here for use with iteration, + // these are also all added as records to the actual type + // + + /** + * List of upgrades (perks, passives, and killstreaks) that the player unlocked at each prestige. + * Each unlock contains a "tier" (set to 0 if the upgrade has no tier), Unix timestamp, and a key. + * The player's first prestige is stored as unlocks, not unlocks_0. + */ + [unlocksIndex: `unlocks${"" | `_${number}`}`]: PitUnlocks; + + /** + * The player's selected perks. + */ + [perk: `selected_perk_${number}`]: string; + + /** + * The amount of gold the player obtained during a prestige. + */ + [key: `cash_during_prestige_${number}`]: number; + + /** + * The player's /pitchat settings. + */ + [chatOption: `chat_option_${keyof typeof PitChatOptions}`]: boolean; + + /** + * Unix timestamps of when the player last claimed various faction items and perks. + */ + [ + genesisClaims: `genesis_weekly_perks_${ + | `perma_${"xp" | "gold"}` + | `claim_item_${PitGenesisFactions}`}` + ]: number; + + /** + * The number of times the player has claimed the Tier VII reward of the respective faction. + */ + [genesisPerma: `genesis_perma_${PitGenesisFactions}`]: number; + + /** + * The player's selected killstreaks. + */ + [killstreak: `selected_killstreak_${number}`]: string; + [stackStreaks: `${"gold" | "xp"}_stack_streak_${number}`]: number; + + xp: number; + cash: number; + renown: number; + + /** + * The current outgoing offers the player has,created with /offer. The objects, each corresponding to one use of the /offer command, contain several keys. + */ + outgoing_offers: PitOutgoingOffer[]; + + /** + * The Unix timestamp of the player's most recent save of their API stats. Updates to the current + * time approximately every three minutes when the player is in Pit. Does not update when it has + * been more than three minutes since the player played Pit. + */ + last_save: number; + + /** + * Stats for the most recent King's Quest the player started. + */ + king_quest: PitKingsQuest; + + /** + * Unix timestamp of when the player last received participation XP. + */ + last_passive_xp: number; + + /** + * ??? + */ + trade_timestamps: []; + + /** + * Byte array of the contents of the player's ender chest. + */ + inv_enderchest: NBTData; + + /** + * Byte arrays of the player's four most recent death recaps. Formatting codes are included in the array. + */ + death_recaps: NBTData; + + /** + * Whether the player has selected to spawn in their faction's base. + */ + genesis_spawn_in_base: boolean; + + /** + * The player's statistics for the in-game Pit leaderboards. + */ + leaderboard_stats: Record< + `Pit_${ + | "blockhead_blocks" + | `tdm_${"red" | "blue"}_kills` + | `kotl_${"time" | "gold"}` + | "rage_pit_damage" + | "kills_as_beast" + | `raffle_${"tickets" | "jackpot"}` + | "auction_bid" + | "cake_eaten"}_20${IntRange<19, 24>}_${"fall" | "winter" | "summer" | "spring"}`, + number + >; + + /** + * The player's currently in-progress contract. + */ + contract: PitContract; + + /** + * Unix timestamp of the player's last completed contract. + */ + last_contract: number; + + /** + * The player's most recently completed trades. + */ + gold_transactions: { + amount: number; + timestamp: number; + }[]; + + /** + * The player's most recently-chosen faction on the Genesis map. + */ + genesis_allegiance: Uppercase; + + /** + * The player's inventory contents. Stored as a byte array. + */ + inv_contents: NBTData; + + /** + * Unix timestamps of when the player last purchased each item in the item shop. + */ + items_last_buy: { + "Pants Bundle": number; + bounty_solvent: number; + combat_spade: number; + diamond_boots: number; + diamond_chestplate: number; + diamond_leggings: number; + diamond_sword: number; + first_aid_egg: number; + golden_pickaxe: number; + iron_pack: number; + jump_boost_potion: number; + new_golden_pickaxe: number; + obsidian: number; + obsidian_stack: number; + tactical_insertion: number; + }; + + /** + * Faction points earned on the most recent iteration of the Genesis map. + */ + genesis_points: number; + + /** + * Array of prestiges. + */ + prestiges: PitPrestiges[]; + + /** + * Byte array of the player's Spire stash inventory, containing the player's items that will be given + * back to them once the Spire event concludes. + */ + spire_stash_inv: NBTData; + + /** + * Byte array of the player's Spire stash armor, containing the player's armor that will be given + * back to them once the Spire event concludes. + */ + spire_stash_armor: NBTData; + + cheap_milk: false; + + /** + * Whether the player has logged in at least once after March 19th, 2018, the date of The Pit 0.3's release. + */ + zero_point_three_gold_transfer: boolean; + + /** + * Contains several objects, each corresponding to a renown unlock. + */ + renown_unlocks: PitRenownUnlocks[]; + + /** + * Unix timestamp of the last time the player disconnected while in combat. + */ + last_midfight_disconnect: number; + + inv_armor: NBTData; + + item_stash: NBTData; + + /** + * Unix timestamp of when the player last selected a faction to join. + */ + genesis_allegiance_time: number; + + /** + * ??? + */ + login_messages: any[]; + + /** + * Array of Minecraft Item IDs corresponding to inventory slots. + * Slots represent the locations items respawn in when the player dies. + */ + hotbar_favorites: [ + number, + number, + number, + number, + number, + number, + number, + number, + number + ]; + + ended_contracts: PitContract[]; + + /** + * List of bounty bumps + */ + bounties: PitBounty[]; + + /** + * Whether the player chooses to receive Night Quests. + */ + night_quests_enabled: boolean; + + /** + * The leaderboard that displays for the player in spawn. + */ + selected_leaderboard: string; + + /** + * The player's selected megastreak. Returns null if the megastreak is Overdrive. + */ + selected_killstreak_0: string; + + /** + * Whether the player is choosing to display their Pit Supporter star. + */ + supporter_star_enabled: boolean; + + /** + * Whether the player chose to disable the default sword and bow from their inventory when they spawn. + */ + disable_spawn_items: boolean; + + shops_throttle: PitShopsThrottle; + + /** + * Array of items that the player has Autobuy enabled on. + */ + autobuy_items: string[]; + + /** + * List of the three contracts the player can select. + * Returns null if the player has no contracts to select + */ + contract_choices: [PitContract?, PitContract?, PitContract?] | null; + + /** + * The player has in their Mystic Well. + */ + mystic_well_item: NBTData; + + /** + * The player's selected megastreak. If they have Uberstreak selected, + * this key returns the player's previously-selected megastreak instead. + */ + selected_megastreak_except_uber: string; + + /** + * Unix timestamps of the player's recent Uberstreak completions. + */ + recent_uberstreaks: number[]; + + /** + * Whether the player's Fancy Hat has an enchanted glint from Pit Supporter. + */ + hat_glint_enabled: boolean; + + /** + * Whether the player has the renown perk Apollo enabled. + */ + apollo_enabled: boolean; + + /** + * Array of events the player helped create in The Pit 0.4. + * Events only appear if the player clicked the "Collect reward" button + * in the Prestige NPC before The Pit 1.0.0. + */ + event_contest_claims: string[]; + + /** + * The stats that the player has the /dropconfirm command in; true for the drop + * confirm being disabled, and false for the drop confirm being enabled. + */ + drop_confirm_disabled: boolean; + + /** + * ??? + */ + zero_point_two_xp: number; + + /** + * Whether the player has the Fancy Hat renown upgrade enabled. + */ + hat_enabled: boolean; + + /** + * Whether the player has the Impatient renown upgrade enabled. + */ + impatient_enabled: boolean; + + /** + * The player's selected launch trail from the renown shop. + */ + selected_launch_trail: string; + + /** + * Whether the player refunded 10 renown from the Golden Pickaxe being + * removed from the renown shop in The Pit 1.0.5. + */ + refunded_golden_pickaxe: boolean; + + /** + * The item in the "sacrifice" slot of the Mystic Well. + */ + mystic_well_pants: NBTData; + + /** + * The Unix time of the player's last use of a Mark of Recon Essence. + */ + reconessence_day: number; + + /** + * The Unix time of when the player last wore dark pants + * with the Lycanthropy enchantment. + */ + last_lycanthropy: number; + + /** + * ??? + */ + uber_paid_up: number; + + /** + * The number of times the player has claimed the +1% + * Mystic Drop Chance Uberdrop from an Uberstreak. + */ + uberdrop_mystic_stacks: number; + + /** + * Whether the player has the renown upgrade Raw Numbers enabled. + */ + raw_numbers_enabled: boolean; + + /** + * @Deprecated + * + * Used by admins to test the contracts feature. + */ + contract_offers: any; +} + +export type HypixelPitProfile = PitProfile & + Record<`unlocks${"" | `_${IntRange<1, 51>}`}`, PitUnlocks> & + Record<`selected_perk_${IntRange<0, 4>}`, string> & + Record<`cash_during_prestige_${IntRange<0, 50>}`, number> & + Record<`chat_option_${keyof typeof PitChatOptions}`, boolean> & + Record< + `genesis_weekly_perks_${ + | `perma_${"xp" | "gold"}` + | `claim_item_${keyof typeof PitGenesisFactions}`}`, + number + > & + Record<`genesis_perma_${keyof typeof PitGenesisFactions}`, number> & + Record<`selected_killstreak_${IntRange<1, 5>}`, string> & + Record<`${"gold" | "xp"}_stack_streak_${IntRange<0, 51>}`, number> & + Record<`selected_killstreak_${IntRange<1, 5>}`, string>; + +export interface HypixelPitStats extends BasicStats { + packages?: ["supporter"]; + restored_inv_backup_1?: number; + stats_move_1: number; + pit_stats_ptl?: PitStatsPTL & unknown; + profile?: HypixelPitProfile; +} diff --git a/packages/hypixel-api-client/tsconfig.json b/packages/hypixel-api-client/tsconfig.json new file mode 100644 index 000000000..564a59900 --- /dev/null +++ b/packages/hypixel-api-client/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 79d611ac6..e81410655 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@nestjs/swagger": "^6.0.1", + "@statsify/hypixel-api-client": "workspace:^", "@statsify/logger": "workspace:^", "@statsify/math": "workspace:^", "@statsify/util": "workspace:^", diff --git a/packages/schemas/src/player/gamemodes/pit/index.ts b/packages/schemas/src/player/gamemodes/pit/index.ts index ab701b78a..bcf675cc8 100644 --- a/packages/schemas/src/player/gamemodes/pit/index.ts +++ b/packages/schemas/src/player/gamemodes/pit/index.ts @@ -6,18 +6,19 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import { APIData, formatTime } from "@statsify/util"; import { Field } from "../../../metadata"; -import { GameModes, IGameModes } from "../../../game"; -import { Progression } from "../../../progression"; -import { add, ratio } from "@statsify/math"; -import { getBounty, getLevel, getLevelFormatted, getPres, getPresReq } from "./util"; export const PIT_MODES = new GameModes([ { api: "overall", hypixel: "PIT", formatted: "Pit" }, ]); export type PitModes = IGameModes; +import { GameModes, IGameModes } from "../../../game"; +import { HypixelPitProfile, HypixelPitStatsPTL } from "@statsify/hypixel-api-client"; +import { Progression } from "../../../progression"; +import { add, ratio } from "@statsify/math"; +import { formatTime } from "@statsify/util"; +import { getBounty, getLevel, getLevelFormatted, getPres, getPresReq } from "./util"; export class Pit { @Field({ @@ -105,7 +106,7 @@ export class Pit { @Field() public joins: number; - public constructor(profile: APIData, data: APIData) { + public constructor(profile: HypixelPitProfile, data: HypixelPitStatsPTL) { this.exp = profile.xp ?? 0; this.gold = profile.cash; this.renown = profile.renown; diff --git a/packages/schemas/src/player/gamemodes/pit/util.ts b/packages/schemas/src/player/gamemodes/pit/util.ts index 4b09dd657..ef2a8d8e7 100644 --- a/packages/schemas/src/player/gamemodes/pit/util.ts +++ b/packages/schemas/src/player/gamemodes/pit/util.ts @@ -6,6 +6,7 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ +import { HypixelPitBounty } from "@statsify/hypixel-api-client"; import { romanNumeral } from "@statsify/util"; const xpMap = [15, 30, 50, 75, 125, 300, 600, 800, 900, 1000, 1200, 1500]; @@ -46,13 +47,6 @@ const levelColors = [ "l§b", ]; -export type Bounty = { - amount: number; - remainingTicks: number; - issuer: string; - timestamp: number; -}; - export const getPres = (xp: number) => { for (let i = 0; i < 50; i++) { if (xp <= presXpReq[i]) { @@ -82,7 +76,7 @@ export const getLevelColor = (level: number) => export const getPresColor = (pres: number) => pres == 0 ? prestigecolors[0] : prestigecolors[Math.floor((pres + 5) / 5)]; -export const getBounty = (bounties: Bounty[]) => +export const getBounty = (bounties: HypixelPitBounty[]) => bounties ? bounties.reduce((p, c) => p + c.amount, 0) : 0; export const getLevelFormatted = (level: number, prestige: number) => { diff --git a/packages/schemas/src/player/index.ts b/packages/schemas/src/player/index.ts index f52fcf9f0..4c9d6259a 100644 --- a/packages/schemas/src/player/index.ts +++ b/packages/schemas/src/player/index.ts @@ -6,9 +6,9 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import { APIData } from "@statsify/util"; import { Color } from "../color"; import { Field } from "../metadata"; +import { HypixelPlayer } from "@statsify/hypixel-api-client"; import { modelOptions as ModelOptions, Severity } from "@typegoose/typegoose"; import { PlayerSocials } from "./socials"; import { PlayerStats } from "./stats"; @@ -82,7 +82,7 @@ export class Player { @Field({ store: { required: false } }) public guildId?: string; - public constructor(data: APIData = {}) { + public constructor(data: HypixelPlayer) { this.uuid = data.uuid; this.username = data.displayname; this.usernameToLower = this.username?.toLowerCase(); diff --git a/packages/schemas/src/player/stats.ts b/packages/schemas/src/player/stats.ts index bdc321f46..7d093df2c 100644 --- a/packages/schemas/src/player/stats.ts +++ b/packages/schemas/src/player/stats.ts @@ -36,7 +36,12 @@ import { } from "./gamemodes"; import { Field } from "../metadata"; import { FormattedGame } from "../game"; -import type { APIData } from "@statsify/util"; +import { + HypixelPitProfile, + HypixelPitStatsPTL, + HypixelPlayer, + HypixelPlayerStats, +} from "@statsify/hypixel-api-client"; export class PlayerStats { @Field({ leaderboard: { fieldName: `${FormattedGame.ARCADE} -` } }) @@ -196,10 +201,10 @@ export class PlayerStats { }) public woolwars: WoolWars; - public constructor(data: APIData = {}) { + public constructor(data: HypixelPlayer) { const achievements = data?.achievements ?? {}; - const stats = data?.stats ?? {}; - const legacy = stats.Legacy ?? {}; + const stats = data?.stats ?? ({} as HypixelPlayerStats); + const legacy = stats?.Legacy ?? {}; this.arcade = new Arcade(stats.Arcade ?? {}, achievements); this.arenabrawl = new ArenaBrawl(stats.Arena ?? {}, legacy); @@ -214,7 +219,10 @@ export class PlayerStats { this.murdermystery = new MurderMystery(stats.MurderMystery ?? {}, achievements); this.paintball = new Paintball(stats.Paintball ?? {}, legacy); this.parkour = new Parkour(data.parkourCompletions ?? {}); - this.pit = new Pit(stats.Pit?.profile ?? {}, stats.Pit?.pit_stats_ptl ?? {}); + this.pit = new Pit( + stats.Pit?.profile ?? ({} as HypixelPitProfile), + stats.Pit?.pit_stats_ptl ?? ({} as HypixelPitStatsPTL) + ); this.quake = new Quake(stats.Quake ?? {}, achievements, legacy); this.quests = new Quests(data.quests ?? {}); this.skywars = new SkyWars(stats.SkyWars ?? {}, achievements); diff --git a/yarn.lock b/yarn.lock index 1f36bf23b..caac98d79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1731,6 +1731,14 @@ __metadata: languageName: unknown linkType: soft +"@statsify/hypixel-api-client@workspace:^, @statsify/hypixel-api-client@workspace:packages/hypixel-api-client": + version: 0.0.0-use.local + resolution: "@statsify/hypixel-api-client@workspace:packages/hypixel-api-client" + dependencies: + "@statsify/util": "workspace:^" + languageName: unknown + linkType: soft + "@statsify/logger@workspace:^, @statsify/logger@workspace:packages/logger": version: 0.0.0-use.local resolution: "@statsify/logger@workspace:packages/logger" @@ -1766,6 +1774,7 @@ __metadata: resolution: "@statsify/schemas@workspace:packages/schemas" dependencies: "@nestjs/swagger": ^6.0.1 + "@statsify/hypixel-api-client": "workspace:^" "@statsify/logger": "workspace:^" "@statsify/math": "workspace:^" "@statsify/util": "workspace:^"