From 4dd571c3672b5fd2e3f6101599676e05109cad9d Mon Sep 17 00:00:00 2001 From: Taken Date: Fri, 22 Aug 2025 13:16:08 +0200 Subject: [PATCH] Added first skywars stats --- .../player/[ign]/_components/Sidebar.tsx | 2 +- .../_stats/skywars/skywars-components.tsx | 51 ++++++++ .../player/[ign]/_stats/skywars/skywars.tsx | 81 +++++++++++++ src/app/(stats)/player/[ign]/page.tsx | 2 + src/data/hypixel/skywars.ts | 109 ++++++++++++++++++ src/lib/hypixel/api/player.ts | 6 +- src/lib/hypixel/skyWarsLevel.ts | 13 +++ src/lib/hypixel/skywars.ts | 30 +++++ src/lib/schema/stats.ts | 9 +- 9 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 src/app/(stats)/player/[ign]/_stats/skywars/skywars-components.tsx create mode 100644 src/app/(stats)/player/[ign]/_stats/skywars/skywars.tsx create mode 100644 src/data/hypixel/skywars.ts create mode 100644 src/lib/hypixel/skyWarsLevel.ts create mode 100644 src/lib/hypixel/skywars.ts diff --git a/src/app/(stats)/player/[ign]/_components/Sidebar.tsx b/src/app/(stats)/player/[ign]/_components/Sidebar.tsx index 438be70..a8f9287 100644 --- a/src/app/(stats)/player/[ign]/_components/Sidebar.tsx +++ b/src/app/(stats)/player/[ign]/_components/Sidebar.tsx @@ -18,7 +18,7 @@ type SidebarProps = { export default function Sidebar({ level, ign, player, guild }: SidebarProps) { return ( - +
diff --git a/src/app/(stats)/player/[ign]/_stats/skywars/skywars-components.tsx b/src/app/(stats)/player/[ign]/_stats/skywars/skywars-components.tsx new file mode 100644 index 0000000..e881df1 --- /dev/null +++ b/src/app/(stats)/player/[ign]/_stats/skywars/skywars-components.tsx @@ -0,0 +1,51 @@ +import { getSkyWarsIcon, getTextColor } from "@/lib/hypixel/skywars" +import { getSkywarsLevel } from "@/lib/hypixel/skyWarsLevel" + +export function SkywarsLevel({ xp, icon }: { xp: number, icon: string | undefined }) { + const level = getSkywarsLevel(xp) + const colors = getTextColor(Math.floor(level)) + const swIcon = getSkyWarsIcon(icon) + const val = `${Math.floor(level)}${swIcon}` + + if (level > 150) { + return ( +

+ [ + + {`${val}`} + + ] +

+ ) + } + + if (level === 50) { +

+ [ + + {`${val}`} + + ] +

+ } + + return ( +

+ [ + {val} + ] +

+ ) +} diff --git a/src/app/(stats)/player/[ign]/_stats/skywars/skywars.tsx b/src/app/(stats)/player/[ign]/_stats/skywars/skywars.tsx new file mode 100644 index 0000000..b978804 --- /dev/null +++ b/src/app/(stats)/player/[ign]/_stats/skywars/skywars.tsx @@ -0,0 +1,81 @@ +"use client" + +import { Card, CardContent } from "@/components/ui/card" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { Separator } from "@/components/ui/separator" +import { formatNumber } from "@/lib/formatters" +import { Player } from "@/lib/schema/player" +import { ChevronDown, ChevronUp } from "lucide-react" +import { useEffect, useRef, useState } from "react" +import CollapsedStats from "../../_components/CollapsedStats" +import { SkywarsLevel } from "./skywars-components" + +export default function SkyWarsStats({ stats }: { stats: Player["player"]["stats"]["SkyWars"] }) { + const ref = useRef(null) + const [opened, setOpened] = useState(false) + + useEffect(() => { + if (!ref.current) return + + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === "attributes" && mutation.attributeName === "data-state") { + const dataState = ref.current?.getAttribute("data-state") + setOpened(dataState === "open") + } + }) + }) + + observer.observe(ref.current, { + attributes: true, + attributeFilter: ["data-state"] + }) + + return () => observer.disconnect() + }, []) + + if (!stats) return null + + const kd = (stats.kills / stats.deaths).toFixed(2) + const wl = (stats.wins / stats.losses).toFixed(2) + + return ( + + + +
+

SkyWars

+
+ Level

, + stat: + }, + { + title:

KD

, + stat:

{kd}

+ }, + { + title:

Wins

, + stat:

{formatNumber(stats.wins)}

+ }, + { + title:

WL

, + stat:

{wl}

+ } + ]} + /> +
+ + {opened === false ? : } + +
+ + + +
+
+
+ ) +} diff --git a/src/app/(stats)/player/[ign]/page.tsx b/src/app/(stats)/player/[ign]/page.tsx index 41099fb..264f444 100644 --- a/src/app/(stats)/player/[ign]/page.tsx +++ b/src/app/(stats)/player/[ign]/page.tsx @@ -7,6 +7,7 @@ import { Loader2Icon } from "lucide-react" import { Suspense } from "react" import Sidebar from "./_components/Sidebar" import BedwarsStats from "./_stats/bedwars/bedwars" +import SkyWarsStats from "./_stats/skywars/skywars" export default async function PlayerPage({ params @@ -73,6 +74,7 @@ async function SuspendedPage({ ign: pign }: { ign: string }) {
+
diff --git a/src/data/hypixel/skywars.ts b/src/data/hypixel/skywars.ts new file mode 100644 index 0000000..dc36051 --- /dev/null +++ b/src/data/hypixel/skywars.ts @@ -0,0 +1,109 @@ +export const TITLE = "SkyWars" +export const INITIAL_XP = [0, 20, 70, 150, 250, 500, 1000, 2000, 3500, 6000, 10000, 15000] +export const RECURRING_XP = 10000 + +export const PRESTIGES = [ + { level: 0, color: "gray", b_color: "gray", name: "None" }, + { level: 5, color: "white", b_color: "white", name: "Iron" }, + { level: 10, color: "gold", b_color: "gold", name: "Gold" }, + { level: 15, color: "aqua", b_color: "aqua", name: "Diamond" }, + { level: 20, color: "dark-green", b_color: "dark-green", name: "Emerald" }, + { level: 25, color: "dark-aqua", b_color: "dark-aqua", name: "Sapphire" }, + { level: 30, color: "dark-red", b_color: "dark-red", name: "Ruby" }, + { level: 35, color: "light-purple", b_color: "light-purple", name: "Crystal" }, + { level: 40, color: "blue", b_color: "blue", name: "Opal" }, + { level: 45, color: "purple", b_color: "purple", name: "Amethyst" }, + { level: 50, color: "rainbow", b_color: "red", name: "Rainbow" }, + { level: 55, color: "white", b_color: "gray", name: "First Class" }, + { level: 60, color: "red", b_color: "dark-red", name: "Assassin" }, + { level: 65, color: "white", b_color: "red", name: "Veteran" }, + { level: 70, color: "gold", b_color: "yellow", name: "God Like" }, + { level: 75, color: "blue", b_color: "white", name: "Warrior" }, + { level: 80, color: "aqua", b_color: "white", name: "Captain" }, + { level: 85, color: "dark-aqua", b_color: "white", name: "Soldier" }, + { level: 90, color: "dark-aqua", b_color: "green", name: "Infantry" }, + { level: 95, color: "yellow", b_color: "red", name: "Sergeant" }, + { level: 100, color: "dark-blue", b_color: "blue", name: "Lieutenant" }, + { level: 105, color: "dark-red", b_color: "gold", name: "Admiral" }, + { level: 110, color: "aqua", b_color: "dark-blue", name: "General" }, + { level: 115, color: "gray", b_color: "dark-gray", name: "Villain" }, + { level: 120, color: "purple", b_color: "light-purple", name: "Skilled" }, + { level: 125, color: "yellow", b_color: "white", name: "Sneaky" }, + { level: 130, color: "yellow", b_color: "red", name: "Overlord" }, + { level: 135, color: "red", b_color: "gold", name: "War Chief" }, + { level: 140, color: "red", b_color: "green", name: "Warlock" }, + { level: 145, color: "aqua", b_color: "green", name: "Emperor" }, + { level: 150, color: "rainbow", b_color: "red", name: "Mythic" } +] + +export const ICONS = { + default: "\u22c6", // ⋆ + angel_1: "\u2605", // ★ + angel_2: "\u2606", // ☆ + angel_3: "\u2055", // ⁕ + angel_4: "\u2736", // ✶ + angel_5: "\u2733", // ✳ + angel_6: "\u2734", // ✴ + angel_7: "\u2737", // ✷ + angel_8: "\u274b", // ❋ + angel_9: "\u273c", // ✼ + angel_10: "\u2742", // ❂ + angel_11: "\u2741", // ❁ + angel_12: "\u262c", // ☬ + iron_prestige: "\u2719", // ✙ + gold_prestige: "\u2764", // ❤ + diamond_prestige: "\u2620", // ☠ + emerald_prestige: "\u2726", // ✦ + sapphire_prestige: "\u270c", // ✌ + ruby_prestige: "\u2766", // ❦ + crystal_prestige: "\u2735", // ✵ + opal_prestige: "\u2763", // ❣ + amethyst_prestige: "\u262f", // ☯ + rainbow_prestige: "\u273a", // ✺ + first_class_prestige: "\u2708", // ✈ + assassin_prestige: "\u26b0", // ⚰ + veteran_prestige: "\u2720", // ✠ + god_like_prestige: "\u2655", // ♕ + warrior_prestige: "\u26a1", // ⚡ + captain_prestige: "\u2042", // ⁂ + soldier_prestige: "\u2730", // ✰ + infantry_prestige: "\u2051", // ⁑ + sergeant_prestige: "\u2622", // ☢ + lieutenant_prestige: "\u2725", // ✥ + admiral_prestige: "\u265d", // ♝ + general_prestige: "\u2646", // ♆ + villain_prestige: "\u2601", // ☁ + skilled_prestige: "\u235f", // ⍟ + sneaky_prestige: "\u2657", // ♗ + overlord_prestige: "", // + war_chief_prestige: "\u265e", // ♞ + warlock_prestige: "", // + emperor_prestige: "\u2748", // ❈ + mythic_prestige: "\u0ca0_\u0ca0", // ಠ_ಠ + favor_icon: "\u2694", // ⚔ + omega_icon: "\u03a9" // Ω +} + +export const MODES = [ + { id: "_ranked", name: "Ranked" }, + { id: "_solo_normal", name: "Solo Normal" }, + { id: "_solo_insane", name: "Solo Insane" }, + { id: "_team_normal", name: "Teams Normal" }, + { id: "_team_insane", name: "Teams Insane" }, + { id: "_mega_normal", name: "Mega" }, + { id: "_mega_doubles", name: "Mega Doubles" }, + { id: "", name: "Overall" } +] + +export const HEADS = [ + { id: "eww", name: "Eww!", color: "darkgray" }, + { id: "yucky", name: "Yucky!", color: "gray" }, + { id: "meh", name: "Meh", color: "white" }, + { id: "decent", name: "Decent", color: "yellow" }, + { id: "salty", name: "Salty", color: "green" }, + { id: "tasty", name: "Tasty", color: "darkaqua" }, + { id: "succulent", name: "Succulent", color: "pink" }, + { id: "sweet", name: "Sweet", color: "aqua" }, + { id: "divine", name: "Divine", color: "gold" }, + { id: "heavenly", name: "Heavenly", color: "purple" } +] diff --git a/src/lib/hypixel/api/player.ts b/src/lib/hypixel/api/player.ts index 590c9b3..380dd29 100644 --- a/src/lib/hypixel/api/player.ts +++ b/src/lib/hypixel/api/player.ts @@ -12,7 +12,11 @@ export async function getPlayer(uuid: string) { if (!res.ok) return null - const { success, data } = playerSchema.safeParse(await res.json()) + const { success, data, error } = playerSchema.safeParse(await res.json()) + + if (error) { + console.log(error) + } if (!success) return null diff --git a/src/lib/hypixel/skyWarsLevel.ts b/src/lib/hypixel/skyWarsLevel.ts new file mode 100644 index 0000000..02d3d7f --- /dev/null +++ b/src/lib/hypixel/skyWarsLevel.ts @@ -0,0 +1,13 @@ +export function getSkywarsLevel(xp: number) { + const xps = [0, 20, 70, 150, 250, 500, 1000, 2000, 3500, 6000, 10000, 15000] + if (xp >= 15000) { + return (xp - 15000) / 10000 + 12 + } + for (let i = 0; i < xps.length; i += 1) { + if (xp < xps[i]) { + return i + (xp - xps[i - 1]) / (xps[i] - xps[i - 1]) + } + } + + return 0 +} diff --git a/src/lib/hypixel/skywars.ts b/src/lib/hypixel/skywars.ts new file mode 100644 index 0000000..3952c71 --- /dev/null +++ b/src/lib/hypixel/skywars.ts @@ -0,0 +1,30 @@ +import { ICONS, PRESTIGES } from "@/data/hypixel/skywars" +import { floorLevel } from "./formatters" + +export function getTextColor(level: number) { + const floored = floorLevel(level, 5) + + if (level > 150) { + return { + text: PRESTIGES.at(-1)?.color ?? "gray", + brackets: PRESTIGES.at(-1)?.b_color ?? "gray" + } + } + + const pres = PRESTIGES.find(p => p.level === floored) + + return { + text: pres?.color ?? "gray", + brackets: pres?.b_color ?? "gray" + } +} + +export function getSkyWarsIcon(icon?: string) { + if (!icon) { + return ICONS.default + } + + const icons = ICONS as Record + + return icons[icon] ?? ICONS.default +} diff --git a/src/lib/schema/stats.ts b/src/lib/schema/stats.ts index f70cbdc..b6374dc 100644 --- a/src/lib/schema/stats.ts +++ b/src/lib/schema/stats.ts @@ -189,4 +189,11 @@ export const bedwarsStatsSchema = z.looseObject({ castle_beds_lost_bedwars: z.number().default(0) }) -export const skywarsStatsSchema = z.looseObject({}) +export const skywarsStatsSchema = z.looseObject({ + skywars_experience: z.number().default(0), + selected_prestige_icon: z.string().optional(), + kills: z.number().default(0), + deaths: z.number().default(0), + wins: z.number().default(0), + losses: z.number().default(0) +})