From f575844479def72ee858c518eda3ad5901237e24 Mon Sep 17 00:00:00 2001
From: pleb
Date: Wed, 29 Oct 2025 21:21:50 -0700
Subject: [PATCH] Build re-usable playerCard component with the skill triangle
---
src/lib/components/HasToolAccess.svelte | 44 +++++-
src/lib/components/PlayerCard.svelte | 131 +++++++++++++++
src/lib/components/PlayerSummaryHeader.svelte | 135 ++++++++++++++++
src/lib/utils/plebsaber-utils.ts | 100 +++++++++++-
.../guides/beatleader-auth/+page.svelte | 11 +-
src/routes/testing/+page.svelte | 149 ++++++------------
src/routes/tools/+layout.server.ts | 63 +++++---
.../tools/beatleader-compare/+page.svelte | 5 +-
.../tools/beatleader-headtohead/+page.svelte | 5 +-
.../beatleader-playlist-gap/+page.svelte | 5 +-
10 files changed, 506 insertions(+), 142 deletions(-)
create mode 100644 src/lib/components/PlayerCard.svelte
create mode 100644 src/lib/components/PlayerSummaryHeader.svelte
diff --git a/src/lib/components/HasToolAccess.svelte b/src/lib/components/HasToolAccess.svelte
index 072925e..aee5f82 100644
--- a/src/lib/components/HasToolAccess.svelte
+++ b/src/lib/components/HasToolAccess.svelte
@@ -3,9 +3,11 @@
formatToolRequirementSummary,
meetsToolRequirement,
DEFAULT_ADMIN_RANK_FALLBACK,
+ PLEB_BEATLEADER_ID,
type BeatLeaderPlayerProfile,
type ToolRequirement
} from '$lib/utils/plebsaber-utils';
+ import PlayerCard from '$lib/components/PlayerCard.svelte';
const BL_PATREON_URL = 'https://www.patreon.com/BeatLeader';
@@ -14,17 +16,19 @@
export let customLockedMessage: string | null = null;
export let showCurrentRank = true;
export let adminRank: number | null = null;
+ export let adminPlayer: BeatLeaderPlayerProfile | null = null;
$: requirementContext = { adminRank };
$: hasAccess = meetsToolRequirement(player, requirement, requirementContext);
$: summary = formatToolRequirementSummary(requirement, requirementContext);
$: fallbackBaseline = DEFAULT_ADMIN_RANK_FALLBACK;
- $: resolvedBaseline = typeof adminRank === 'number' && Number.isFinite(adminRank) && adminRank > 0
- ? adminRank
- : fallbackBaseline;
+ $: resolvedBaseline =
+ typeof adminRank === 'number' && Number.isFinite(adminRank) && adminRank > 0 ? adminRank : fallbackBaseline;
+ $: plebProfile = adminPlayer;
+ $: plebName = plebProfile?.name;
$: baselineCopy = resolvedBaseline
- ? `players ranked better than pleb (#${resolvedBaseline.toLocaleString()})`
- : 'players ranked better than pleb';
+ ? `players ranked better than ${plebName} (#${resolvedBaseline.toLocaleString()})`
+ : `players ranked better than ${plebName}`;
$: defaultLockedMessage = requirement?.requiresBetterRankThanAdmin
? `You must be a BL Patreon supporter or ${baselineCopy} to use this tool.`
: requirement?.lockedMessage ?? null;
@@ -47,15 +51,43 @@
{#if showLockedMessage}
{showLockedMessage}
{/if}
+ {#if plebProfile}
+
+ {/if}
{#if showCurrentRank}
{#if playerRankDisplay}
Current global rank: {playerRankDisplay}
{:else}
- We couldn't determine your current BeatLeader rank. Refresh after your profile updates.
+ We couldn't determine your current BeatLeader rank. Try logging in.
{/if}
{/if}
{/if}
+
+
diff --git a/src/lib/components/PlayerCard.svelte b/src/lib/components/PlayerCard.svelte
new file mode 100644
index 0000000..1b86725
--- /dev/null
+++ b/src/lib/components/PlayerCard.svelte
@@ -0,0 +1,131 @@
+
+
+
+
+
+
diff --git a/src/lib/components/PlayerSummaryHeader.svelte b/src/lib/components/PlayerSummaryHeader.svelte
new file mode 100644
index 0000000..efd9a49
--- /dev/null
+++ b/src/lib/components/PlayerSummaryHeader.svelte
@@ -0,0 +1,135 @@
+
+
+
+
+
+
diff --git a/src/lib/utils/plebsaber-utils.ts b/src/lib/utils/plebsaber-utils.ts
index b91d5fa..0a539ce 100644
--- a/src/lib/utils/plebsaber-utils.ts
+++ b/src/lib/utils/plebsaber-utils.ts
@@ -46,6 +46,9 @@ export type BeatLeaderPlayerProfile = {
country?: string | null;
rank?: number | null;
countryRank?: number | null;
+ techPp?: number | null;
+ accPp?: number | null;
+ passPp?: number | null;
};
export type RequirementContext = {
@@ -64,6 +67,23 @@ export type Difficulty = {
characteristic: string;
};
+export type TriangleCorners = {
+ tech: { x: number; y: number };
+ acc: { x: number; y: number };
+ pass: { x: number; y: number };
+};
+
+export type TriangleNormalized = {
+ tech: number;
+ acc: number;
+ pass: number;
+};
+
+export type TriangleData = {
+ corners: TriangleCorners;
+ normalized: TriangleNormalized;
+} | null;
+
// ============================================================================
// 2. Constants
// ============================================================================
@@ -413,7 +433,81 @@ export function percentile(values: number[], p: number): number {
}
// ============================================================================
-// 6. Playlist Generation
+// 6. Skill Triangle Calculations
+// ============================================================================
+
+const DEFAULT_MAX_TECH_PP = 1300;
+const DEFAULT_MAX_ACC_PP = 15000;
+const DEFAULT_MAX_PASS_PP = 6000;
+const GYRON_LENGTH = 57.74;
+
+const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
+
+/**
+ * Calculate skill triangle data from player's PP values
+ * Returns corner coordinates and normalized values for rendering the skill triangle
+ */
+export function calculateSkillTriangle(
+ techPp: number | null | undefined,
+ accPp: number | null | undefined,
+ passPp: number | null | undefined
+): TriangleData {
+ const tech = typeof techPp === 'number' ? techPp : 0;
+ const acc = typeof accPp === 'number' ? accPp : 0;
+ const pass = typeof passPp === 'number' ? passPp : 0;
+
+ // If all values are zero, return null
+ if (tech === 0 && acc === 0 && pass === 0) {
+ return null;
+ }
+
+ // Calculate triangle scale
+ const triangleScale = Math.max(
+ 1,
+ tech > 0 ? tech / DEFAULT_MAX_TECH_PP : 0,
+ acc > 0 ? acc / DEFAULT_MAX_ACC_PP : 0,
+ pass > 0 ? pass / DEFAULT_MAX_PASS_PP : 0
+ );
+
+ const maxTechPp = DEFAULT_MAX_TECH_PP * triangleScale;
+ const maxAccPp = DEFAULT_MAX_ACC_PP * triangleScale;
+ const maxPassPp = DEFAULT_MAX_PASS_PP * triangleScale;
+
+ // Normalize PP values
+ const normalizedTechPp = maxTechPp ? clamp(tech / maxTechPp, 0, 1) : 0;
+ const normalizedAccPp = maxAccPp ? clamp(acc / maxAccPp, 0, 1) : 0;
+ const normalizedPassPp = maxPassPp ? clamp(pass / maxPassPp, 0, 1) : 0;
+
+ // Calculate corner positions
+ const cornerTech = {
+ x: (GYRON_LENGTH - normalizedTechPp * GYRON_LENGTH) * 0.866,
+ y: 86.6 - (GYRON_LENGTH - normalizedTechPp * GYRON_LENGTH) / 2
+ };
+ const cornerAcc = {
+ x: 100 - (GYRON_LENGTH - normalizedAccPp * GYRON_LENGTH) * 0.866,
+ y: 86.6 - (GYRON_LENGTH - normalizedAccPp * GYRON_LENGTH) / 2
+ };
+ const cornerPass = {
+ x: 50,
+ y: (86.6 - GYRON_LENGTH / 2) * (1 - normalizedPassPp)
+ };
+
+ return {
+ corners: {
+ tech: cornerTech,
+ acc: cornerAcc,
+ pass: cornerPass
+ },
+ normalized: {
+ tech: normalizedTechPp,
+ acc: normalizedAccPp,
+ pass: normalizedPassPp
+ }
+ };
+}
+
+// ============================================================================
+// 7. Playlist Generation
// ============================================================================
/**
@@ -472,7 +566,7 @@ export function downloadPlaylist(playlistData: unknown): void {
}
// ============================================================================
-// 7. Pagination Utilities
+// 8. Pagination Utilities
// ============================================================================
export type PaginationResult = {
@@ -503,7 +597,7 @@ export function calculatePagination(
}
// ============================================================================
-// 8. URL Parameter Utilities
+// 9. URL Parameter Utilities
// ============================================================================
/**
diff --git a/src/routes/guides/beatleader-auth/+page.svelte b/src/routes/guides/beatleader-auth/+page.svelte
index cd84807..0772504 100644
--- a/src/routes/guides/beatleader-auth/+page.svelte
+++ b/src/routes/guides/beatleader-auth/+page.svelte
@@ -27,8 +27,12 @@
Educational use only: The information and resources on this page are for learning purposes. Do not use them for real authentication or accessing accounts.
- This app supports three ways to access your BeatLeader data: Steam, OAuth, and a website‑style session.
+ For this app, I explored three ways to access your BeatLeader data: Steam, OAuth, or a website‑style session.
+
+ I wanted tools like Compare Players to show unranked star ratings when your BeatLeader account is a supporter and ShowAllRatings is enabled. That turns out to not be possible without implementing Steam ticket handling using the Steamworks SDK. The rest of the notes here were written before I realized that.
+
+
@@ -82,11 +86,6 @@
Default API auth is Steam . You can override per request using ?auth=steam|oauth|session|auto|none.
-
- Tools like Compare Players
- can show unranked star ratings when your BeatLeader account is a supporter and ShowAllRatings is enabled.
-
-
Steam Login
Authenticate via Steam OpenID to link your Steam account. Then use the BeatLeader Auth Tool with your Steam session ticket to capture a website session.
diff --git a/src/routes/testing/+page.svelte b/src/routes/testing/+page.svelte
index e4d4eec..00bb1e6 100644
--- a/src/routes/testing/+page.svelte
+++ b/src/routes/testing/+page.svelte
@@ -1,5 +1,6 @@