Must be ranked higher than pleb to use tools

This commit is contained in:
2025-10-29 15:56:42 -07:00
parent f59db0021d
commit 5daf221cd7
12 changed files with 420 additions and 23 deletions
+17 -4
View File
@@ -2,6 +2,7 @@
import {
formatToolRequirementSummary,
meetsToolRequirement,
DEFAULT_ADMIN_RANK_FALLBACK,
type BeatLeaderPlayerProfile,
type ToolRequirement
} from '$lib/utils/plebsaber-utils';
@@ -12,10 +13,22 @@
export let requirement: ToolRequirement | null = null;
export let customLockedMessage: string | null = null;
export let showCurrentRank = true;
export let adminRank: number | null = null;
$: hasAccess = meetsToolRequirement(player, requirement);
$: summary = formatToolRequirementSummary(requirement);
$: lockedMessage = customLockedMessage ?? requirement?.lockedMessage ?? 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;
$: baselineCopy = resolvedBaseline
? `players ranked better than pleb (#${resolvedBaseline.toLocaleString()})`
: 'players ranked better than pleb';
$: defaultLockedMessage = requirement?.requiresBetterRankThanAdmin
? `You must be a BL Patreon supporter or ${baselineCopy} to use this tool.`
: requirement?.lockedMessage ?? null;
$: lockedMessage = customLockedMessage ?? defaultLockedMessage;
$: showLockedMessage = lockedMessage && lockedMessage !== summary ? lockedMessage : null;
$: playerRank = typeof player?.rank === 'number' ? player?.rank ?? null : null;
$: playerRankDisplay = playerRank !== null ? `#${playerRank.toLocaleString()}` : null;
@@ -26,7 +39,7 @@
{:else}
<div class="mt-6 rounded-md border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-amber-100 leading-relaxed">
<p class="font-semibold text-amber-200">
Tools are restricted to <a class="underline hover:text-white" href={BL_PATREON_URL} target="_blank" rel="noreferrer noopener">BeatLeader supporters</a> (and the top 3k ranked players).
Tools are restricted to <a class="underline hover:text-white" href={BL_PATREON_URL} target="_blank" rel="noreferrer noopener">BeatLeader supporters</a> (and {baselineCopy}).
</p>
{#if summary}
<p class="mt-1">{summary}</p>
+36 -5
View File
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { dev } from '$app/environment';
import beatleaderLogo from '$lib/assets/beatleader-logo.png';
import { DEFAULT_ADMIN_RANK_FALLBACK, PLEB_BEATLEADER_ID } from '$lib/utils/plebsaber-utils';
const links = [
{ href: '/', label: 'Home' },
{ href: '/tools', label: 'Tools' },
@@ -37,6 +37,8 @@
let loginHref = '/auth/beatleader/login';
let checkingSession = true;
let menuOpen = false;
let adminRank: number | null = null;
let adminFallbackRank = DEFAULT_ADMIN_RANK_FALLBACK;
const getProfileUrl = (id?: string) => (id ? `https://beatleader.com/u/${encodeURIComponent(id)}` : 'https://beatleader.com');
@@ -64,6 +66,18 @@
};
}
function extractAdminBaseline(payload: unknown): { rank: number | null; fallback: number } | null {
if (!payload || typeof payload !== 'object') return null;
const baseline = (payload as { adminBaseline?: { rank?: unknown; fallback?: unknown } }).adminBaseline;
if (!baseline || typeof baseline !== 'object') return null;
const rank = (baseline as { rank?: unknown }).rank;
const fallback = (baseline as { fallback?: unknown }).fallback;
return {
rank: typeof rank === 'number' && Number.isFinite(rank) && rank > 0 ? rank : null,
fallback: typeof fallback === 'number' && Number.isFinite(fallback) && fallback > 0 ? fallback : DEFAULT_ADMIN_RANK_FALLBACK
};
}
onMount(() => {
const redirectTarget = `${window.location.pathname}${window.location.search}${window.location.hash}` || '/';
loginHref = `/auth/beatleader/login?redirect_uri=${encodeURIComponent(redirectTarget)}`;
@@ -77,6 +91,11 @@
if (profile) {
user = profile;
}
const baseline = extractAdminBaseline(json);
if (baseline) {
adminRank = baseline.rank;
adminFallbackRank = baseline.fallback;
}
} else if (res.status === 401) {
try {
const body = (await res.json()) as Record<string, unknown>;
@@ -103,6 +122,12 @@
function closeMenu() {
menuOpen = false;
}
$: baselineRank = (adminRank ?? adminFallbackRank ?? DEFAULT_ADMIN_RANK_FALLBACK);
$: requiresWarning = typeof user?.rank === 'number' && Number.isFinite(user.rank)
? (user.rank as number) > baselineRank
: false;
$: baselineCopy = baselineRank ? `pleb (#${baselineRank.toLocaleString()})` : 'pleb';
</script>
<header class="sticky top-0 z-40 backdrop-blur supports-[backdrop-filter]:bg-surface/50 border-b border-white/10">
@@ -142,12 +167,15 @@
</button>
{#if menuOpen}
<div class="dropdown" role="menu">
{#if typeof user.rank === 'number' && user.rank > 3000}
{#if requiresWarning}
<div class="dropdown-warning">
<strong>Heads up:</strong>
<span>Tools are limited to <a class="underline" href="https://www.patreon.com/BeatLeader" target="_blank" rel="noreferrer noopener">BeatLeader supporters</a> or players ranked in the global top 3k.</span>
<span>Tools are limited to <a class="underline" href="https://www.patreon.com/BeatLeader" target="_blank" rel="noreferrer noopener">BeatLeader supporters</a> or players ranked better than {baselineCopy}.</span>
</div>
{/if}
{#if user?.id === PLEB_BEATLEADER_ID}
<a href="/tools/stats" class="dropdown-item" on:click={closeMenu}>Stats</a>
{/if}
{#if dev}
<a href="/testing" class="dropdown-item" on:click={closeMenu}>Testing</a>
{/if}
@@ -185,12 +213,15 @@
<span class="text-sm text-muted">Connecting…</span>
{:else if user}
<div class="border border-white/10 rounded-md overflow-hidden">
{#if typeof user.rank === 'number' && user.rank > 3000}
{#if requiresWarning}
<div class="dropdown-warning mobile">
<strong>Heads up:</strong>
<span>Tools are limited to <a class="underline" href="https://www.patreon.com/BeatLeader" target="_blank" rel="noreferrer noopener">BeatLeader supporters</a> or players inside the global top 3k.</span>
<span>Tools are limited to <a class="underline" href="https://www.patreon.com/BeatLeader" target="_blank" rel="noreferrer noopener">BeatLeader supporters</a> or players ranked better than {baselineCopy}.</span>
</div>
{/if}
{#if user?.id === PLEB_BEATLEADER_ID}
<a href="/tools/stats" class="dropdown-item" on:click={close}>Stats</a>
{/if}
{#if dev}
<a href="/testing" class="dropdown-item" on:click={close}>Testing</a>
{/if}
+5
View File
@@ -50,6 +50,11 @@ function writeSessions(sessions: Record<string, StoredSession>): void {
}
}
export function getAllSessions(): StoredSession[] {
const sessions = readSessions();
return Object.values(sessions);
}
function baseCookieOptions() {
return {
path: '/',
+39 -6
View File
@@ -48,8 +48,13 @@ export type BeatLeaderPlayerProfile = {
countryRank?: number | null;
};
export type RequirementContext = {
adminRank?: number | null;
};
export type ToolRequirement = {
minGlobalRank?: number;
requiresBetterRankThanAdmin?: boolean;
summary: string;
lockedMessage?: string;
};
@@ -64,15 +69,17 @@ export type Difficulty = {
// ============================================================================
export const ONE_YEAR_SECONDS = 365 * 24 * 60 * 60;
export const PLEB_BEATLEADER_ID = '76561199407393962';
export const DEFAULT_ADMIN_RANK_FALLBACK = 3000;
export const DIFFICULTIES = ['Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus'] as const;
export const MODES = ['Standard', 'Lawless', 'OneSaber', 'NoArrows', 'Lightshow'] as const;
const DEFAULT_PRIVATE_TOOL_REQUIREMENT: ToolRequirement = {
minGlobalRank: 3000,
summary: 'BeatLeader global rank within the top 3000',
lockedMessage: 'You must be a BL Patreon supporter or remain ranked in the global top 3k to use this tool.'
requiresBetterRankThanAdmin: true,
summary: 'Ranked higher than pleb on BeatLeader',
lockedMessage: 'You must either contribute to BL Patreon (or be ranked higher than pleb) to use this tool'
};
export const TOOL_REQUIREMENTS = {
@@ -89,19 +96,45 @@ export function getToolRequirement(key: string): ToolRequirement | null {
export function meetsToolRequirement(
profile: BeatLeaderPlayerProfile | null | undefined,
requirement: ToolRequirement | null | undefined
requirement: ToolRequirement | null | undefined,
context?: RequirementContext
): boolean {
if (!requirement) return true;
const rank = profile?.rank ?? null;
if (requirement.requiresBetterRankThanAdmin) {
const baseline = resolveAdminBaseline(context);
if (typeof rank !== 'number' || !Number.isFinite(rank) || rank <= 0) return false;
return rank <= (baseline ?? DEFAULT_ADMIN_RANK_FALLBACK);
}
if (requirement.minGlobalRank !== undefined) {
const rank = profile?.rank ?? null;
if (typeof rank !== 'number' || !Number.isFinite(rank) || rank <= 0) return false;
return rank <= requirement.minGlobalRank;
}
return true;
}
export function formatToolRequirementSummary(requirement: ToolRequirement | null | undefined): string {
function resolveAdminBaseline(context?: RequirementContext): number | null {
const val = context?.adminRank;
if (typeof val === 'number' && Number.isFinite(val) && val > 0) {
return val;
}
return null;
}
export function formatToolRequirementSummary(
requirement: ToolRequirement | null | undefined,
context?: RequirementContext
): string {
if (!requirement) return '';
if (requirement.requiresBetterRankThanAdmin) {
const baseline = resolveAdminBaseline(context);
if (baseline) {
return `BeatLeader global rank ≤ ${baseline.toLocaleString()} (pleb's current rank)`;
}
return requirement.summary || `BeatLeader global rank ≤ ${DEFAULT_ADMIN_RANK_FALLBACK.toLocaleString()}`;
}
if (requirement.summary) return requirement.summary;
if (requirement.minGlobalRank !== undefined) {
return `BeatLeader global rank ≤ ${requirement.minGlobalRank}`;