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 @@ + + +
+ Avatar +
+
{name}
+
+ {#if country} + {country} + {/if} + {#if showRank && typeof rank === 'number'} + Rank: {rank} + {/if} +
+
+ {#if hasTriangle} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {/if} +
+ + + 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 @@ + + +
+ Avatar +
+
{name}
+
+ {#if country} + {country} + {/if} + {#if showRank && typeof rank === 'number'} + Rank: {rank} + {/if} +
+
+ {#if hasTriangle} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {/if} +
+ + + 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. +

+