Add XP and Level leaderboards

This commit is contained in:
2026-02-11 21:22:05 -07:00
parent 4294472730
commit 8fb5193073
2 changed files with 130 additions and 37 deletions

View File

@@ -8,7 +8,7 @@ import axios from 'axios'
import { GetIconForUser } from '@/util/bd' import { GetIconForUser } from '@/util/bd'
import Image from 'next/image' import Image from 'next/image'
interface LeaderboardEntry { interface BaseEntry {
id: number id: number
username: string username: string
value: number value: number
@@ -19,29 +19,115 @@ interface LeaderboardEntry {
customIcon: string | null customIcon: string | null
} }
interface Stats {
highScore: string
totalNormalBerries: string
totalPoisonBerries: string
totalSlowBerries: string
totalUltraBerries: string
totalSpeedyBerries: string
totalCoinBerries: string
totalRandomBerries: string
totalAntiBerries: string
totalGoldenBerries: string
coins: string
}
interface LeaderboardEntry extends BaseEntry {
type: 'leaderboard'
value: number
}
interface Account extends BaseEntry {
type: 'account'
stats: Stats
xp: bigint
}
export function calculateXP (
normalBerries: bigint,
poisonBerries: bigint,
slowBerries: bigint,
ultraBerries: bigint,
speedyBerries: bigint,
coinBerries: bigint,
randomBerries: bigint,
antiBerries: bigint,
goldenBerries: bigint
): bigint {
let totalXp = 0n
totalXp += normalBerries
totalXp -= poisonBerries
totalXp -= slowBerries
totalXp += ultraBerries * 5n
totalXp += speedyBerries * 10n
totalXp += coinBerries * 10n
totalXp += randomBerries
totalXp -= antiBerries
totalXp += goldenBerries * 4n
if (totalXp < 0n) totalXp = 0n
return totalXp
}
export function calculateLevel (xp: bigint): number {
const levelDivisor = 50.0
const xpNumber = Number(xp)
const discriminant = 95 * 95 + levelDivisor * 2 * xpNumber
const level = (-95 + Math.sqrt(discriminant)) / levelDivisor
return Math.floor(level) + 1
}
export default function BerryDashLeaderboards () { export default function BerryDashLeaderboards () {
const [selected, setSelected] = useState<number>(-1) const [selected, setSelected] = useState<number>(-1)
const [selectedBerryOption, setSelectedBerryOption] = useState<number>(0) const [selectedBerryOption, setSelectedBerryOption] = useState<number>(0)
const [gettingEntries, setGettingEntries] = useState<boolean>(false) const [gettingEntries, setGettingEntries] = useState<boolean>(false)
const [entries, setEntries] = useState<LeaderboardEntry[]>([]) const [entries, setEntries] = useState<(LeaderboardEntry | Account)[]>([])
const Refresh = useCallback(async () => { const Refresh = useCallback(async () => {
setGettingEntries(true) setGettingEntries(true)
try { try {
const result = await axios.get( if (selected == 3 || selected == 4) {
'/api/berrydash/leaderboard/' + const result = await axios.get('/api/berrydash/account?username=')
(selected == 0 if (result.data.success) {
? 'score' let accounts = result.data.data as Account[]
: selected == 1
? 'berry?berry=' + selectedBerryOption accounts = accounts.map(acc => {
: selected == 2 const xp = calculateXP(
? 'coin' BigInt(acc.stats.totalNormalBerries),
: selected == 3 BigInt(acc.stats.totalPoisonBerries),
? 'legacy' BigInt(acc.stats.totalSlowBerries),
: 'total') BigInt(acc.stats.totalUltraBerries),
) BigInt(acc.stats.totalSpeedyBerries),
if (result.data.success) { BigInt(acc.stats.totalCoinBerries),
setEntries(result.data.data) BigInt(acc.stats.totalRandomBerries),
BigInt(acc.stats.totalAntiBerries),
BigInt(acc.stats.totalGoldenBerries)
)
return { ...acc, xp }
})
accounts.sort((a, b) => (b.xp > a.xp ? 1 : b.xp < a.xp ? -1 : 0))
setEntries(accounts)
}
} else {
const result = await axios.get(
'/api/berrydash/leaderboard/' +
(selected == 0
? 'score'
: selected == 1
? 'berry?berry=' + selectedBerryOption
: selected == 2
? 'coin'
: selected == 5
? 'legacy'
: 'total')
)
if (result.data.success) {
setEntries(result.data.data as LeaderboardEntry[])
}
} }
} catch { } catch {
setEntries([]) setEntries([])
@@ -82,14 +168,10 @@ export default function BerryDashLeaderboards () {
<button onClick={() => setSelected(0)}>Score Leaderboard</button> <button onClick={() => setSelected(0)}>Score Leaderboard</button>
<button onClick={() => setSelected(1)}>Berry Leaderboard</button> <button onClick={() => setSelected(1)}>Berry Leaderboard</button>
<button onClick={() => setSelected(2)}>Coins Leaderboard</button> <button onClick={() => setSelected(2)}>Coins Leaderboard</button>
<button onClick={() => setSelected(3)} disabled={true}> <button onClick={() => setSelected(3)}>Level Leaderboard</button>
Level Leaderboard <button onClick={() => setSelected(4)}>Total XP Leaderboard</button>
</button>
<button onClick={() => setSelected(4)} disabled={true}>
Total XP Leaderboard
</button>
<button onClick={() => setSelected(5)}>Legacy Leaderboard</button> <button onClick={() => setSelected(5)}>Legacy Leaderboard</button>
<button onClick={() => setSelected(4)}> <button onClick={() => setSelected(6)}>
Total Berries Leaderboard Total Berries Leaderboard
</button> </button>
</div> </div>
@@ -98,6 +180,9 @@ export default function BerryDashLeaderboards () {
<> <>
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
{entries.map((item, index) => { {entries.map((item, index) => {
const isAccount = 'stats' in item
const isLeaderboard = 'value' in item && !isAccount
return ( return (
<div <div
key={item.id} key={item.id}
@@ -108,32 +193,40 @@ export default function BerryDashLeaderboards () {
src={ src={
!item.customIcon !item.customIcon
? `https://games-r2.lncvrt.xyz/game-assets/berrydash/icons/bird_${ ? `https://games-r2.lncvrt.xyz/game-assets/berrydash/icons/bird_${
item.icon == 1 item.icon === 1
? GetIconForUser(item.id) ? GetIconForUser(item.id)
: item.icon : item.icon
}.png` }.png`
: '/api/berrydash/icon-marketplace/icon?id=' + : `/api/berrydash/icon-marketplace/icon?id=${item.customIcon}&raw=true`
item.customIcon +
'&raw=true'
} }
className='pointer-events-none' className='pointer-events-none'
width={48} width={48}
height={48} height={48}
alt='' alt=''
unoptimized={true} unoptimized
/> />
<p> <p>
{item.username} (#{index + 1}) {item.username} (#{index + 1})
</p> </p>
</div> </div>
<p>
{selected == 1 || selected == 4 {isLeaderboard ? (
? 'Berries' <p>
: selected == 2 {selected === 1 || selected === 6
? 'Coins' ? 'Berries'
: 'Score'} : selected === 2
: {item.value.toLocaleString('en-US')} ? 'Coins'
</p> : 'Score'}
: {item.value.toLocaleString('en-US')}
</p>
) : isAccount ? (
<p>
{selected === 3 ? 'Level' : 'XP'}:{' '}
{selected === 3
? calculateLevel(item.xp).toLocaleString('en-US')
: item.xp.toLocaleString('en-US')}
</p>
) : null}
</div> </div>
) )
})} })}

View File

@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "es2024",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,