add beatleader score fetching and add icons to FC
Some checks failed
deploy / deploy (push) Failing after 2s
Some checks failed
deploy / deploy (push) Failing after 2s
This commit is contained in:
parent
2e93a1b27f
commit
80e6c0da43
@ -1,3 +1,4 @@
|
|||||||
|
import { AppProvider } from "@/components/AppProvider";
|
||||||
import { ssrSettings } from "@/ssrSettings";
|
import { ssrSettings } from "@/ssrSettings";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
@ -40,7 +41,7 @@ export default function RootLayout({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children}
|
<AppProvider>{children}</AppProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ import Scores from "@/components/player/Scores";
|
|||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { useSettingsStore } from "@/store/settingsStore";
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
import { SortType, SortTypes } from "@/types/SortTypes";
|
import { SortType, SortTypes } from "@/types/SortTypes";
|
||||||
import { getPlayerInfo } from "@/utils/scoresaber/api";
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayerInfo(params.id).then((playerResponse) => {
|
ScoreSaberAPI.getPlayerInfo(params.id).then((playerResponse) => {
|
||||||
if (!playerResponse) {
|
if (!playerResponse) {
|
||||||
setError(true);
|
setError(true);
|
||||||
setErrorMessage("Failed to fetch player. Is the ID correct?");
|
setErrorMessage("Failed to fetch player. Is the ID correct?");
|
||||||
|
@ -8,7 +8,7 @@ import { Spinner } from "@/components/Spinner";
|
|||||||
import PlayerRanking from "@/components/player/PlayerRanking";
|
import PlayerRanking from "@/components/player/PlayerRanking";
|
||||||
import PlayerRankingMobile from "@/components/player/PlayerRankingMobile";
|
import PlayerRankingMobile from "@/components/player/PlayerRankingMobile";
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { fetchTopPlayers } from "@/utils/scoresaber/api";
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
import { normalizedRegionName } from "@/utils/utils";
|
import { normalizedRegionName } from "@/utils/utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
@ -53,7 +53,7 @@ export default function RankingCountry({ params }: RankingCountryProps) {
|
|||||||
const updatePage = useCallback(
|
const updatePage = useCallback(
|
||||||
(page: any) => {
|
(page: any) => {
|
||||||
console.log("Switching page to", page);
|
console.log("Switching page to", page);
|
||||||
fetchTopPlayers(page, country).then((response) => {
|
ScoreSaberAPI.fetchTopPlayers(page, country).then((response) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
setError(true);
|
setError(true);
|
||||||
setErrorMessage("No players found");
|
setErrorMessage("No players found");
|
||||||
|
@ -8,7 +8,7 @@ import { Spinner } from "@/components/Spinner";
|
|||||||
import PlayerRanking from "@/components/player/PlayerRanking";
|
import PlayerRanking from "@/components/player/PlayerRanking";
|
||||||
import PlayerRankingMobile from "@/components/player/PlayerRankingMobile";
|
import PlayerRankingMobile from "@/components/player/PlayerRankingMobile";
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { fetchTopPlayers } from "@/utils/scoresaber/api";
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
import { GlobeAsiaAustraliaIcon } from "@heroicons/react/20/solid";
|
import { GlobeAsiaAustraliaIcon } from "@heroicons/react/20/solid";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
@ -46,7 +46,7 @@ export default function RankingGlobal() {
|
|||||||
const updatePage = useCallback(
|
const updatePage = useCallback(
|
||||||
(page: any) => {
|
(page: any) => {
|
||||||
console.log("Switching page to", page);
|
console.log("Switching page to", page);
|
||||||
fetchTopPlayers(page).then((response) => {
|
ScoreSaberAPI.fetchTopPlayers(page).then((response) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
setError(true);
|
setError(true);
|
||||||
setErrorMessage("No players found");
|
setErrorMessage("No players found");
|
||||||
|
24
src/components/AppProvider.tsx
Normal file
24
src/components/AppProvider.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useBeatLeaderScoresStore } from "@/store/beatLeaderScoresStore";
|
||||||
|
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
||||||
|
|
||||||
|
type AppProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AppProvider({ children }: AppProviderProps) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UPDATE_INTERVAL = 1000 * 60 * 15; // 15 minutes
|
||||||
|
|
||||||
|
useBeatLeaderScoresStore.getState().updatePlayerScores();
|
||||||
|
setInterval(() => {
|
||||||
|
useBeatLeaderScoresStore.getState().updatePlayerScores();
|
||||||
|
}, UPDATE_INTERVAL);
|
||||||
|
|
||||||
|
useScoresaberScoresStore.getState().updatePlayerScores();
|
||||||
|
setInterval(() => {
|
||||||
|
useScoresaberScoresStore.getState().updatePlayerScores();
|
||||||
|
}, UPDATE_INTERVAL);
|
@ -1,5 +1,6 @@
|
|||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { usePlayerScoresStore } from "@/store/playerScoresStore";
|
import { useBeatLeaderScoresStore } from "@/store/beatLeaderScoresStore";
|
||||||
|
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
||||||
import { useSettingsStore } from "@/store/settingsStore";
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
import { formatNumber } from "@/utils/number";
|
import { formatNumber } from "@/utils/number";
|
||||||
import {
|
import {
|
||||||
@ -28,7 +29,7 @@ type PlayerInfoProps = {
|
|||||||
export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
||||||
const playerId = playerData.id;
|
const playerId = playerData.id;
|
||||||
const settingsStore = useStore(useSettingsStore, (store) => store);
|
const settingsStore = useStore(useSettingsStore, (store) => store);
|
||||||
const playerScoreStore = useStore(usePlayerScoresStore, (store) => store);
|
const playerScoreStore = useStore(useScoresaberScoresStore, (store) => store);
|
||||||
|
|
||||||
// Whether we have scores for this player in the local database
|
// Whether we have scores for this player in the local database
|
||||||
const hasLocalScores = playerScoreStore?.exists(playerId);
|
const hasLocalScores = playerScoreStore?.exists(playerId);
|
||||||
@ -50,7 +51,8 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addProfile(isFriend: boolean) {
|
async function addProfile(isFriend: boolean) {
|
||||||
if (!usePlayerScoresStore.getState().exists(playerId)) {
|
const setupScoresaber = async () => {
|
||||||
|
if (!useScoresaberScoresStore.getState().exists(playerId)) {
|
||||||
const reponse = await playerScoreStore?.addPlayer(
|
const reponse = await playerScoreStore?.addPlayer(
|
||||||
playerId,
|
playerId,
|
||||||
(page, totalPages) => {
|
(page, totalPages) => {
|
||||||
@ -58,7 +60,7 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
|||||||
|
|
||||||
if (page == 1) {
|
if (page == 1) {
|
||||||
toastId.current = toast.info(
|
toastId.current = toast.info(
|
||||||
`Fetching scores ${page}/${totalPages}`,
|
`Fetching ScoreSaber scores ${page}/${totalPages}`,
|
||||||
{
|
{
|
||||||
autoClose: autoClose,
|
autoClose: autoClose,
|
||||||
progress: page / totalPages,
|
progress: page / totalPages,
|
||||||
@ -67,13 +69,13 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
|||||||
} else {
|
} else {
|
||||||
toast.update(toastId.current, {
|
toast.update(toastId.current, {
|
||||||
progress: page / totalPages,
|
progress: page / totalPages,
|
||||||
render: `Fetching scores ${page}/${totalPages}`,
|
render: `Fetching ScoreSaber scores ${page}/${totalPages}`,
|
||||||
autoClose: autoClose,
|
autoClose: autoClose,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Fetching scores for ${playerId} (${page}/${totalPages})`,
|
`Fetching ScoreSaber scores for ${playerId} (${page}/${totalPages})`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -83,6 +85,46 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupBeatleader = async () => {
|
||||||
|
if (!useBeatLeaderScoresStore.getState().exists(playerId)) {
|
||||||
|
const reponse = await playerScoreStore?.addPlayer(
|
||||||
|
playerId,
|
||||||
|
(page, totalPages) => {
|
||||||
|
const autoClose = page == totalPages ? 5000 : false;
|
||||||
|
|
||||||
|
if (page == 1) {
|
||||||
|
toastId.current = toast.info(
|
||||||
|
`Fetching BeatLeader scores ${page}/${totalPages}`,
|
||||||
|
{
|
||||||
|
autoClose: autoClose,
|
||||||
|
progress: page / totalPages,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.update(toastId.current, {
|
||||||
|
progress: page / totalPages,
|
||||||
|
render: `Fetching BeatLeader scores ${page}/${totalPages}`,
|
||||||
|
autoClose: autoClose,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Fetching BeatLeader scores for ${playerId} (${page}/${totalPages})`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (reponse?.error) {
|
||||||
|
toast.error("Failed to fetch scores");
|
||||||
|
console.log(reponse.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await setupScoresaber();
|
||||||
|
await setupBeatleader();
|
||||||
|
|
||||||
if (!isFriend) {
|
if (!isFriend) {
|
||||||
toast.success(`Successfully set ${playerData.name} as your profile`);
|
toast.success(`Successfully set ${playerData.name} as your profile`);
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard";
|
import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard";
|
||||||
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { ScoresaberScore } from "@/schemas/scoresaber/score";
|
import { ScoresaberScore } from "@/schemas/scoresaber/score";
|
||||||
|
import { useBeatLeaderScoresStore } from "@/store/beatLeaderScoresStore";
|
||||||
import { formatNumber } from "@/utils/number";
|
import { formatNumber } from "@/utils/number";
|
||||||
import { GlobeAsiaAustraliaIcon } from "@heroicons/react/20/solid";
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
GlobeAsiaAustraliaIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
} from "@heroicons/react/20/solid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@ -9,11 +15,17 @@ import ScoreStatLabel from "./ScoreStatLabel";
|
|||||||
|
|
||||||
type ScoreProps = {
|
type ScoreProps = {
|
||||||
score: ScoresaberScore;
|
score: ScoresaberScore;
|
||||||
|
player: ScoresaberPlayer;
|
||||||
leaderboard: ScoresaberLeaderboardInfo;
|
leaderboard: ScoresaberLeaderboardInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Score({ score, leaderboard }: ScoreProps) {
|
export default function Score({ score, player, leaderboard }: ScoreProps) {
|
||||||
const isFullCombo = score.missedNotes + score.badCuts === 0;
|
const isFullCombo = score.missedNotes + score.badCuts === 0;
|
||||||
|
const beatleaderScoreData = useBeatLeaderScoresStore
|
||||||
|
.getState()
|
||||||
|
.getScore(player.id, leaderboard.songHash);
|
||||||
|
|
||||||
|
console.log(beatleaderScoreData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 pb-2 pt-2 first:pt-0 last:pb-0 md:grid-cols-[1.1fr_6fr_3fr] xl:md:grid-cols-[.95fr_6fr_3fr]">
|
<div className="grid grid-cols-1 pb-2 pt-2 first:pt-0 last:pb-0 md:grid-cols-[1.1fr_6fr_3fr] xl:md:grid-cols-[.95fr_6fr_3fr]">
|
||||||
@ -90,7 +102,13 @@ export default function Score({ score, leaderboard }: ScoreProps) {
|
|||||||
"min-w-[2rem]",
|
"min-w-[2rem]",
|
||||||
isFullCombo ? "bg-green-500" : "bg-red-500",
|
isFullCombo ? "bg-green-500" : "bg-red-500",
|
||||||
)}
|
)}
|
||||||
title={`${score.missedNotes} missed notes. ${score.badCuts} bad cuts.`}
|
icon={
|
||||||
|
isFullCombo ? (
|
||||||
|
<CheckIcon width={20} height={20} />
|
||||||
|
) : (
|
||||||
|
<XMarkIcon width={20} height={20} />
|
||||||
|
)
|
||||||
|
}
|
||||||
value={
|
value={
|
||||||
isFullCombo
|
isFullCombo
|
||||||
? "FC"
|
? "FC"
|
||||||
|
@ -2,7 +2,7 @@ import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
|||||||
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
||||||
import { useSettingsStore } from "@/store/settingsStore";
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
import { SortType, SortTypes } from "@/types/SortTypes";
|
import { SortType, SortTypes } from "@/types/SortTypes";
|
||||||
import { fetchScores } from "@/utils/scoresaber/api";
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Card from "../Card";
|
import Card from "../Card";
|
||||||
@ -43,7 +43,8 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) {
|
|||||||
const updateScoresPage = useCallback(
|
const updateScoresPage = useCallback(
|
||||||
(sortType: SortType, page: any) => {
|
(sortType: SortType, page: any) => {
|
||||||
console.log(`Switching page to ${page} with sort ${sortType.value}`);
|
console.log(`Switching page to ${page} with sort ${sortType.value}`);
|
||||||
fetchScores(playerId, page, sortType.value, 10).then((scoresResponse) => {
|
ScoreSaberAPI.fetchScores(playerId, page, sortType.value, 10).then(
|
||||||
|
(scoresResponse) => {
|
||||||
if (!scoresResponse) {
|
if (!scoresResponse) {
|
||||||
setError(true);
|
setError(true);
|
||||||
setErrorMessage("No Scores");
|
setErrorMessage("No Scores");
|
||||||
@ -74,7 +75,8 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) {
|
|||||||
scroll: false,
|
scroll: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[playerId, router, scores],
|
[playerId, router, scores],
|
||||||
);
|
);
|
||||||
@ -125,7 +127,12 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) {
|
|||||||
const { score, leaderboard } = scoreData;
|
const { score, leaderboard } = scoreData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Score key={id} score={score} leaderboard={leaderboard} />
|
<Score
|
||||||
|
key={id}
|
||||||
|
player={playerData}
|
||||||
|
score={score}
|
||||||
|
leaderboard={leaderboard}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { formatNumber } from "@/utils/number";
|
import { formatNumber } from "@/utils/number";
|
||||||
import { getPlayerInfo, searchByName } from "@/utils/scoresaber/api";
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@ -27,14 +27,14 @@ export default function SearchPlayer() {
|
|||||||
const id = search.split("/").pop();
|
const id = search.split("/").pop();
|
||||||
if (id == undefined) return;
|
if (id == undefined) return;
|
||||||
|
|
||||||
const player = await getPlayerInfo(id);
|
const player = await ScoreSaberAPI.getPlayerInfo(id);
|
||||||
if (player == undefined) return;
|
if (player == undefined) return;
|
||||||
|
|
||||||
setPlayers([player]);
|
setPlayers([player]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search by name
|
// Search by name
|
||||||
const players = await searchByName(search);
|
const players = await ScoreSaberAPI.searchByName(search);
|
||||||
if (players == undefined) return;
|
if (players == undefined) return;
|
||||||
|
|
||||||
setPlayers(players);
|
setPlayers(players);
|
||||||
|
30
src/schemas/beatleader/difficulty.ts
Normal file
30
src/schemas/beatleader/difficulty.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { BeatleaderModifierRating } from "./modifierRating";
|
||||||
|
import { BeatleaderModifier } from "./modifiers";
|
||||||
|
|
||||||
|
export type BeatleaderDifficulty = {
|
||||||
|
id: number;
|
||||||
|
value: number;
|
||||||
|
mode: number;
|
||||||
|
difficultyName: string;
|
||||||
|
modeName: string;
|
||||||
|
status: number;
|
||||||
|
modifierValues: BeatleaderModifier;
|
||||||
|
modifiersRating: BeatleaderModifierRating;
|
||||||
|
nominatedTime: number;
|
||||||
|
qualifiedTime: number;
|
||||||
|
rankedTime: number;
|
||||||
|
stars: number;
|
||||||
|
predictedAcc: number;
|
||||||
|
passRating: number;
|
||||||
|
accRating: number;
|
||||||
|
techRating: number;
|
||||||
|
type: number;
|
||||||
|
njs: number;
|
||||||
|
nps: number;
|
||||||
|
notes: number;
|
||||||
|
bombs: number;
|
||||||
|
walls: number;
|
||||||
|
maxScore: number;
|
||||||
|
duration: number;
|
||||||
|
requirements: number;
|
||||||
|
};
|
16
src/schemas/beatleader/leaderboard.ts
Normal file
16
src/schemas/beatleader/leaderboard.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { BeatleaderDifficulty } from "./difficulty";
|
||||||
|
import { BeatleaderSong } from "./song";
|
||||||
|
|
||||||
|
export type BeatleaderLeaderboard = {
|
||||||
|
id: string;
|
||||||
|
song: BeatleaderSong;
|
||||||
|
difficulty: BeatleaderDifficulty;
|
||||||
|
scores: null; // ??
|
||||||
|
changes: null; // ??
|
||||||
|
qualification: null; // ??
|
||||||
|
reweight: null; // ??
|
||||||
|
leaderboardGroup: null; // ??
|
||||||
|
plays: number;
|
||||||
|
clan: null; // ??
|
||||||
|
clanRankingContested: boolean;
|
||||||
|
};
|
5
src/schemas/beatleader/metadata.ts
Normal file
5
src/schemas/beatleader/metadata.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type BeatleaderMetadata = {
|
||||||
|
itemsPerPage: number;
|
||||||
|
page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
18
src/schemas/beatleader/modifierRating.ts
Normal file
18
src/schemas/beatleader/modifierRating.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export type BeatleaderModifierRating = {
|
||||||
|
id: number;
|
||||||
|
fsPredictedAcc: number;
|
||||||
|
fsPassRating: number;
|
||||||
|
fsAccRating: number;
|
||||||
|
fsTechRating: number;
|
||||||
|
fsStars: number;
|
||||||
|
ssPredictedAcc: number;
|
||||||
|
ssPassRating: number;
|
||||||
|
ssAccRating: number;
|
||||||
|
ssTechRating: number;
|
||||||
|
ssStars: number;
|
||||||
|
sfPredictedAcc: number;
|
||||||
|
sfPassRating: number;
|
||||||
|
sfAccRating: number;
|
||||||
|
sfTechRating: number;
|
||||||
|
sfStars: number;
|
||||||
|
};
|
16
src/schemas/beatleader/modifiers.ts
Normal file
16
src/schemas/beatleader/modifiers.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export type BeatleaderModifier = {
|
||||||
|
modifierId: number;
|
||||||
|
da: number;
|
||||||
|
fs: number;
|
||||||
|
sf: number;
|
||||||
|
ss: number;
|
||||||
|
gn: number;
|
||||||
|
na: number;
|
||||||
|
nb: number;
|
||||||
|
nf: number;
|
||||||
|
no: number;
|
||||||
|
pm: number;
|
||||||
|
sc: number;
|
||||||
|
sa: number;
|
||||||
|
op: number;
|
||||||
|
};
|
51
src/schemas/beatleader/score.ts
Normal file
51
src/schemas/beatleader/score.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { BeatleaderLeaderboard } from "./leaderboard";
|
||||||
|
import { BeatleaderScoreImprovement } from "./scoreImprovement";
|
||||||
|
import { BeatleaderScoreOffsets } from "./scoreOffsets";
|
||||||
|
|
||||||
|
export type BeatleaderScore = {
|
||||||
|
myScore: null; // ??
|
||||||
|
validContexts: number;
|
||||||
|
leaderboard: BeatleaderLeaderboard;
|
||||||
|
contextExtensions: null; // ??
|
||||||
|
accLeft: number;
|
||||||
|
accRight: number;
|
||||||
|
id: number;
|
||||||
|
baseScore: number;
|
||||||
|
modifiedScore: number;
|
||||||
|
accuracy: number;
|
||||||
|
playerId: string;
|
||||||
|
pp: number;
|
||||||
|
bonusPp: number;
|
||||||
|
passPP: number;
|
||||||
|
accPP: number;
|
||||||
|
techPP: number;
|
||||||
|
rank: number;
|
||||||
|
country: string;
|
||||||
|
fcAccuracy: number;
|
||||||
|
fcPp: number;
|
||||||
|
weight: number;
|
||||||
|
replay: string;
|
||||||
|
modifiers: string;
|
||||||
|
badCuts: number;
|
||||||
|
missedNotes: number;
|
||||||
|
bombCuts: number;
|
||||||
|
wallsHit: number;
|
||||||
|
pauses: number;
|
||||||
|
fullCombo: boolean;
|
||||||
|
platform: string;
|
||||||
|
maxCombo: number;
|
||||||
|
maxStreak: number;
|
||||||
|
hmd: number;
|
||||||
|
controller: number;
|
||||||
|
leaderboardId: string;
|
||||||
|
timeset: string;
|
||||||
|
timepost: number;
|
||||||
|
replaysWatched: number;
|
||||||
|
playCount: number;
|
||||||
|
priority: number;
|
||||||
|
player: null; // ??
|
||||||
|
scoreImprovement: BeatleaderScoreImprovement;
|
||||||
|
rankVoting: null; // ??
|
||||||
|
metadata: null; // ??
|
||||||
|
offsets: BeatleaderScoreOffsets;
|
||||||
|
};
|
19
src/schemas/beatleader/scoreImprovement.ts
Normal file
19
src/schemas/beatleader/scoreImprovement.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export type BeatleaderScoreImprovement = {
|
||||||
|
id: number;
|
||||||
|
timeset: number;
|
||||||
|
score: number;
|
||||||
|
accuracy: number;
|
||||||
|
pp: number;
|
||||||
|
bonusPp: number;
|
||||||
|
rank: number;
|
||||||
|
accRight: number;
|
||||||
|
accLeft: number;
|
||||||
|
averageRankedAccuracy: number;
|
||||||
|
totalPp: number;
|
||||||
|
totalRank: number;
|
||||||
|
badCuts: number;
|
||||||
|
missedNotes: number;
|
||||||
|
bombCuts: number;
|
||||||
|
wallsHit: number;
|
||||||
|
pauses: number;
|
||||||
|
};
|
8
src/schemas/beatleader/scoreOffsets.ts
Normal file
8
src/schemas/beatleader/scoreOffsets.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export type BeatleaderScoreOffsets = {
|
||||||
|
id: number;
|
||||||
|
frames: number;
|
||||||
|
notes: number;
|
||||||
|
walls: number;
|
||||||
|
heights: number;
|
||||||
|
pauses: number;
|
||||||
|
};
|
0
src/schemas/beatleader/scores.ts
Normal file
0
src/schemas/beatleader/scores.ts
Normal file
5
src/schemas/beatleader/smaller/smallerLeaderboard.ts
Normal file
5
src/schemas/beatleader/smaller/smallerLeaderboard.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { BeatleaderSmallerSong } from "./smallerSong";
|
||||||
|
|
||||||
|
export type BeatleaderSmallerLeaderboard = {
|
||||||
|
song: BeatleaderSmallerSong;
|
||||||
|
};
|
14
src/schemas/beatleader/smaller/smallerScore.ts
Normal file
14
src/schemas/beatleader/smaller/smallerScore.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { BeatleaderSmallerLeaderboard } from "./smallerLeaderboard";
|
||||||
|
import { BeatleaderSmallerScoreImprovement } from "./smallerScoreImprovement";
|
||||||
|
|
||||||
|
export type BeatleaderSmallerScore = {
|
||||||
|
id: number;
|
||||||
|
timepost: number;
|
||||||
|
accLeft: number;
|
||||||
|
accRight: number;
|
||||||
|
fcAccuracy: number;
|
||||||
|
wallsHit: number;
|
||||||
|
replay: string;
|
||||||
|
leaderboard: BeatleaderSmallerLeaderboard;
|
||||||
|
scoreImprovement: BeatleaderSmallerScoreImprovement | null;
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
export type BeatleaderSmallerScoreImprovement = {
|
||||||
|
score: number;
|
||||||
|
accuracy: number;
|
||||||
|
accRight: number;
|
||||||
|
accLeft: number;
|
||||||
|
badCuts: number;
|
||||||
|
missedNotes: number;
|
||||||
|
bombCuts: number;
|
||||||
|
};
|
4
src/schemas/beatleader/smaller/smallerSong.ts
Normal file
4
src/schemas/beatleader/smaller/smallerSong.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type BeatleaderSmallerSong = {
|
||||||
|
hash: string;
|
||||||
|
bpm: number;
|
||||||
|
};
|
16
src/schemas/beatleader/song.ts
Normal file
16
src/schemas/beatleader/song.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export type BeatleaderSong = {
|
||||||
|
id: string;
|
||||||
|
hash: string;
|
||||||
|
name: string;
|
||||||
|
subName: string;
|
||||||
|
author: string;
|
||||||
|
mapperId: string;
|
||||||
|
coverImage: string;
|
||||||
|
fullCoverImage: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
bpm: number;
|
||||||
|
duration: number;
|
||||||
|
tags: string;
|
||||||
|
uploadTime: number;
|
||||||
|
difficulties: null; // ??
|
||||||
|
};
|
12
src/schemas/scoresaber/smaller/smallerLeaderboard.ts
Normal file
12
src/schemas/scoresaber/smaller/smallerLeaderboard.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ScoresaberDifficulty } from "../difficulty";
|
||||||
|
|
||||||
|
export type ScoresaberSmallerLeaderboardInfo = {
|
||||||
|
id: string;
|
||||||
|
songHash: string;
|
||||||
|
difficulty: ScoresaberDifficulty;
|
||||||
|
maxScore: number;
|
||||||
|
createdDate: string;
|
||||||
|
stars: number;
|
||||||
|
plays: number;
|
||||||
|
coverImage: string;
|
||||||
|
};
|
7
src/schemas/scoresaber/smaller/smallerPlayerScore.ts
Normal file
7
src/schemas/scoresaber/smaller/smallerPlayerScore.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ScoresaberSmallerLeaderboardInfo } from "./smallerLeaderboard";
|
||||||
|
import { ScoresaberSmallerScore } from "./smallerScore";
|
||||||
|
|
||||||
|
export type ScoresaberSmallerPlayerScore = {
|
||||||
|
score: ScoresaberSmallerScore;
|
||||||
|
leaderboard: ScoresaberSmallerLeaderboardInfo;
|
||||||
|
};
|
16
src/schemas/scoresaber/smaller/smallerScore.ts
Normal file
16
src/schemas/scoresaber/smaller/smallerScore.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export type ScoresaberSmallerScore = {
|
||||||
|
id: number;
|
||||||
|
rank: number;
|
||||||
|
baseScore: number;
|
||||||
|
modifiedScore: number;
|
||||||
|
pp: number;
|
||||||
|
weight: number;
|
||||||
|
modifiers: string;
|
||||||
|
multiplier: number;
|
||||||
|
badCuts: number;
|
||||||
|
missedNotes: number;
|
||||||
|
maxCombo: number;
|
||||||
|
fullCombo: boolean;
|
||||||
|
hmd: number;
|
||||||
|
timeSet: string;
|
||||||
|
};
|
347
src/store/beatLeaderScoresStore.ts
Normal file
347
src/store/beatLeaderScoresStore.ts
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { BeatleaderSmallerScore } from "@/schemas/beatleader/smaller/smallerScore";
|
||||||
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
|
import { BeatLeaderAPI } from "@/utils/beatleader/api";
|
||||||
|
import moment from "moment";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
import { useSettingsStore } from "./settingsStore";
|
||||||
|
|
||||||
|
type Player = {
|
||||||
|
id: string;
|
||||||
|
scores: BeatleaderSmallerScore[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BeatLeaderScoresStore {
|
||||||
|
lastUpdated: number;
|
||||||
|
players: Player[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets when the player scores were last updated
|
||||||
|
*
|
||||||
|
* @param lastUpdated when the player scores were last updated
|
||||||
|
*/
|
||||||
|
setLastUpdated: (lastUpdated: number) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the player exists
|
||||||
|
*
|
||||||
|
* @param playerId the player id
|
||||||
|
* @returns if the player exists
|
||||||
|
*/
|
||||||
|
exists: (playerId: string) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the given player
|
||||||
|
*
|
||||||
|
* @param playerId the player id
|
||||||
|
* @returns the player
|
||||||
|
*/
|
||||||
|
get(playerId: string): Player | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the score for the given player and song hash
|
||||||
|
*
|
||||||
|
* @param playerId the player id
|
||||||
|
* @param songHash the song hash
|
||||||
|
*/
|
||||||
|
getScore(
|
||||||
|
playerId: string,
|
||||||
|
songHash: string,
|
||||||
|
): BeatleaderSmallerScore | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the player to the local database
|
||||||
|
*
|
||||||
|
* @param playerId the player id
|
||||||
|
* @param callback a callback to call when a score page is fetched
|
||||||
|
* @returns if the player was added successfully
|
||||||
|
*/
|
||||||
|
addPlayer: (
|
||||||
|
playerId: string,
|
||||||
|
callback?: (page: number, totalPages: number) => void,
|
||||||
|
) => Promise<{
|
||||||
|
error: boolean;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the player scores and adds any new scores to the local database
|
||||||
|
*/
|
||||||
|
updatePlayerScores: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes
|
||||||
|
|
||||||
|
export const useBeatLeaderScoresStore = create<BeatLeaderScoresStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
lastUpdated: 0,
|
||||||
|
players: [],
|
||||||
|
|
||||||
|
setLastUpdated: (lastUpdated: number) => {
|
||||||
|
set({ lastUpdated });
|
||||||
|
},
|
||||||
|
|
||||||
|
exists: (playerId: string) => {
|
||||||
|
const players: Player[] = useBeatLeaderScoresStore.getState().players;
|
||||||
|
return players.some((player) => player.id == playerId);
|
||||||
|
},
|
||||||
|
|
||||||
|
get: (playerId: string) => {
|
||||||
|
const players: Player[] = useBeatLeaderScoresStore.getState().players;
|
||||||
|
return players.find((player) => player.id == playerId);
|
||||||
|
},
|
||||||
|
|
||||||
|
getScore: (playerId: string, songHash: string) => {
|
||||||
|
const player = useBeatLeaderScoresStore.getState().get(playerId);
|
||||||
|
if (player == undefined) return undefined;
|
||||||
|
|
||||||
|
return player.scores.find(
|
||||||
|
(score) => score.leaderboard.song.hash == songHash,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
addPlayer: async (
|
||||||
|
playerId: string,
|
||||||
|
callback?: (
|
||||||
|
page: number,
|
||||||
|
totalPages: number,
|
||||||
|
leaderboardName: string,
|
||||||
|
) => void,
|
||||||
|
) => {
|
||||||
|
const players = useBeatLeaderScoresStore.getState().players;
|
||||||
|
|
||||||
|
// Check if the player already exists
|
||||||
|
if (useBeatLeaderScoresStore.getState().exists(playerId)) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: "Player already exists",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all of the players scores
|
||||||
|
const scores = await BeatLeaderAPI.fetchAllScores(
|
||||||
|
playerId,
|
||||||
|
(page, totalPages) => {
|
||||||
|
if (callback) callback(page, totalPages, "BeatLeader");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (scores == undefined) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: "Could not fetch beatleader scores for player",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let smallerScores = new Array<BeatleaderSmallerScore>();
|
||||||
|
for (const score of scores) {
|
||||||
|
// We have to do this to limit the amount of data we store
|
||||||
|
// so we don't exceed the local storage limit
|
||||||
|
smallerScores.push({
|
||||||
|
id: score.id,
|
||||||
|
accLeft: score.accLeft,
|
||||||
|
accRight: score.accRight,
|
||||||
|
fcAccuracy: score.fcAccuracy,
|
||||||
|
wallsHit: score.wallsHit,
|
||||||
|
replay: score.replay,
|
||||||
|
leaderboard: {
|
||||||
|
song: {
|
||||||
|
bpm: score.leaderboard.song.bpm,
|
||||||
|
hash: score.leaderboard.song.hash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scoreImprovement:
|
||||||
|
score.scoreImprovement != null
|
||||||
|
? {
|
||||||
|
score: score.scoreImprovement.score,
|
||||||
|
accuracy: score.scoreImprovement.accuracy,
|
||||||
|
accRight: score.scoreImprovement.accRight,
|
||||||
|
accLeft: score.scoreImprovement.accLeft,
|
||||||
|
badCuts: score.scoreImprovement.badCuts,
|
||||||
|
missedNotes: score.scoreImprovement.missedNotes,
|
||||||
|
bombCuts: score.scoreImprovement.bombCuts,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
timepost: score.timepost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove scores that are already in the database
|
||||||
|
const player = useBeatLeaderScoresStore.getState().get(playerId);
|
||||||
|
if (player) {
|
||||||
|
smallerScores = smallerScores.filter(
|
||||||
|
(score) => player.scores.findIndex((s) => s.id == score.id) == -1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
players: [
|
||||||
|
...players,
|
||||||
|
{
|
||||||
|
id: playerId,
|
||||||
|
scores: smallerScores,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
message: "Player added successfully",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePlayerScores: async () => {
|
||||||
|
const players = useBeatLeaderScoresStore.getState().players;
|
||||||
|
const friends = useSettingsStore.getState().friends;
|
||||||
|
|
||||||
|
let allPlayers = new Array<ScoresaberPlayer>();
|
||||||
|
for (const friend of friends) {
|
||||||
|
allPlayers.push(friend);
|
||||||
|
}
|
||||||
|
const localPlayer = useSettingsStore.getState().player;
|
||||||
|
if (localPlayer) {
|
||||||
|
allPlayers.push(localPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add local player and friends if they don't exist
|
||||||
|
for (const player of allPlayers) {
|
||||||
|
if (useBeatLeaderScoresStore.getState().get(player.id) == undefined) {
|
||||||
|
toast.info(
|
||||||
|
`${
|
||||||
|
player.id == localPlayer?.id
|
||||||
|
? `You were`
|
||||||
|
: `Friend ${player.name} was`
|
||||||
|
} missing from the BeatLeader scores database, adding...`,
|
||||||
|
);
|
||||||
|
await useBeatLeaderScoresStore.getState().addPlayer(player.id);
|
||||||
|
toast.success(
|
||||||
|
`${
|
||||||
|
player.id == useSettingsStore.getState().player?.id
|
||||||
|
? `You were`
|
||||||
|
: `Friend ${player.name} was`
|
||||||
|
} added to the BeatLeader scores database`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if we refreshed the scores recently
|
||||||
|
const timeUntilRefreshMs =
|
||||||
|
UPDATE_INTERVAL -
|
||||||
|
(Date.now() - useBeatLeaderScoresStore.getState().lastUpdated);
|
||||||
|
if (timeUntilRefreshMs > 0) {
|
||||||
|
console.log(
|
||||||
|
"Waiting",
|
||||||
|
moment.duration(timeUntilRefreshMs).humanize(),
|
||||||
|
"to refresh scores for players",
|
||||||
|
);
|
||||||
|
setTimeout(
|
||||||
|
() => useBeatLeaderScoresStore.getState().updatePlayerScores(),
|
||||||
|
timeUntilRefreshMs,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through all of the players and update their scores
|
||||||
|
for (const player of players) {
|
||||||
|
if (player == undefined) continue;
|
||||||
|
console.log(`Updating scores for ${player.id}...`);
|
||||||
|
|
||||||
|
let newPlayers = players;
|
||||||
|
let oldScores = player.scores;
|
||||||
|
|
||||||
|
// Sort the scores by date (newset to oldest), so we know when to stop searching for new scores
|
||||||
|
oldScores = oldScores.sort((a, b) => {
|
||||||
|
return a.timepost - b.timepost;
|
||||||
|
});
|
||||||
|
if (!oldScores.length) return;
|
||||||
|
|
||||||
|
const mostRecentScore = oldScores[0];
|
||||||
|
let search = true;
|
||||||
|
|
||||||
|
let page = 0;
|
||||||
|
let newScoresCount = 0;
|
||||||
|
while (search) {
|
||||||
|
page++;
|
||||||
|
const newScores = await BeatLeaderAPI.fetchScores(player.id, page);
|
||||||
|
if (newScores == undefined) continue;
|
||||||
|
|
||||||
|
for (const score of newScores.scores) {
|
||||||
|
if (mostRecentScore && score.id == mostRecentScore.id) {
|
||||||
|
search = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the old score
|
||||||
|
const oldScoreIndex = oldScores.findIndex(
|
||||||
|
(score) => score.id == score.id,
|
||||||
|
);
|
||||||
|
if (oldScoreIndex != -1) {
|
||||||
|
oldScores = oldScores.splice(oldScoreIndex, 1);
|
||||||
|
}
|
||||||
|
oldScores.push({
|
||||||
|
id: score.id,
|
||||||
|
accLeft: score.accLeft,
|
||||||
|
accRight: score.accRight,
|
||||||
|
fcAccuracy: score.fcAccuracy,
|
||||||
|
wallsHit: score.wallsHit,
|
||||||
|
replay: score.replay,
|
||||||
|
leaderboard: {
|
||||||
|
song: {
|
||||||
|
bpm: score.leaderboard.song.bpm,
|
||||||
|
hash: score.leaderboard.song.hash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scoreImprovement:
|
||||||
|
score.scoreImprovement != null
|
||||||
|
? {
|
||||||
|
score: score.scoreImprovement.score,
|
||||||
|
accuracy: score.scoreImprovement.accuracy,
|
||||||
|
accRight: score.scoreImprovement.accRight,
|
||||||
|
accLeft: score.scoreImprovement.accLeft,
|
||||||
|
badCuts: score.scoreImprovement.badCuts,
|
||||||
|
missedNotes: score.scoreImprovement.missedNotes,
|
||||||
|
bombCuts: score.scoreImprovement.bombCuts,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
timepost: score.timepost,
|
||||||
|
});
|
||||||
|
newScoresCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the player if it already exists
|
||||||
|
newPlayers = newPlayers.filter((playerr) => playerr.id != player.id);
|
||||||
|
// Add the player
|
||||||
|
newPlayers.push({
|
||||||
|
id: player.id,
|
||||||
|
scores: oldScores,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newScoresCount > 0) {
|
||||||
|
console.log(
|
||||||
|
`Found ${newScoresCount} new beatleader scores for ${player.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
players: newPlayers,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
});
|
||||||
|
console.log(friends);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "beatleaderScores",
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
version: 2,
|
||||||
|
|
||||||
|
migrate: (state: any, version: number) => {
|
||||||
|
state.scores = [];
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
import { ScoresaberSmallerPlayerScore } from "@/schemas/scoresaber/smaller/smallerPlayerScore";
|
||||||
import { fetchAllScores, fetchScores } from "@/utils/scoresaber/api";
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
@ -12,11 +12,11 @@ import { useSettingsStore } from "./settingsStore";
|
|||||||
type Player = {
|
type Player = {
|
||||||
id: string;
|
id: string;
|
||||||
scores: {
|
scores: {
|
||||||
scoresaber: ScoresaberPlayerScore[];
|
scoresaber: ScoresaberSmallerPlayerScore[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PlayerScoresStore {
|
interface ScoreSaberScoresStore {
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
players: Player[];
|
players: Player[];
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ interface PlayerScoresStore {
|
|||||||
|
|
||||||
const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes
|
const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes
|
||||||
|
|
||||||
export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
@ -77,12 +77,12 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
exists: (playerId: string) => {
|
exists: (playerId: string) => {
|
||||||
const players: Player[] = usePlayerScoresStore.getState().players;
|
const players: Player[] = useScoresaberScoresStore.getState().players;
|
||||||
return players.some((player) => player.id == playerId);
|
return players.some((player) => player.id == playerId);
|
||||||
},
|
},
|
||||||
|
|
||||||
get: (playerId: string) => {
|
get: (playerId: string) => {
|
||||||
const players: Player[] = usePlayerScoresStore.getState().players;
|
const players: Player[] = useScoresaberScoresStore.getState().players;
|
||||||
return players.find((player) => player.id == playerId);
|
return players.find((player) => player.id == playerId);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -90,10 +90,10 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
playerId: string,
|
playerId: string,
|
||||||
callback?: (page: number, totalPages: number) => void,
|
callback?: (page: number, totalPages: number) => void,
|
||||||
) => {
|
) => {
|
||||||
const players = usePlayerScoresStore.getState().players;
|
const players = useScoresaberScoresStore.getState().players;
|
||||||
|
|
||||||
// Check if the player already exists
|
// Check if the player already exists
|
||||||
if (usePlayerScoresStore.getState().exists(playerId)) {
|
if (useScoresaberScoresStore.getState().exists(playerId)) {
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
message: "Player already exists",
|
message: "Player already exists",
|
||||||
@ -101,23 +101,53 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all of the players scores
|
// Get all of the players scores
|
||||||
let scores = await fetchAllScores(
|
let scores = await ScoreSaberAPI.fetchAllScores(
|
||||||
playerId,
|
playerId,
|
||||||
"recent",
|
"recent",
|
||||||
(page, totalPages) => {
|
(page, totalPages) => {
|
||||||
if (callback) callback(page, totalPages);
|
if (callback) callback(page, totalPages);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (scores == undefined) {
|
if (scores == undefined) {
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
message: "Could not fetch scores for player",
|
message: "Could not fetch scores for player",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
let smallerScores = new Array<ScoresaberSmallerPlayerScore>();
|
||||||
|
for (const score of scores) {
|
||||||
|
smallerScores.push({
|
||||||
|
score: {
|
||||||
|
id: score.score.id,
|
||||||
|
rank: score.score.rank,
|
||||||
|
baseScore: score.score.baseScore,
|
||||||
|
modifiedScore: score.score.modifiedScore,
|
||||||
|
pp: score.score.pp,
|
||||||
|
weight: score.score.weight,
|
||||||
|
modifiers: score.score.modifiers,
|
||||||
|
multiplier: score.score.multiplier,
|
||||||
|
badCuts: score.score.badCuts,
|
||||||
|
missedNotes: score.score.missedNotes,
|
||||||
|
maxCombo: score.score.maxCombo,
|
||||||
|
fullCombo: score.score.fullCombo,
|
||||||
|
hmd: score.score.hmd,
|
||||||
|
timeSet: score.score.timeSet,
|
||||||
|
},
|
||||||
|
leaderboard: {
|
||||||
|
id: score.leaderboard.id,
|
||||||
|
songHash: score.leaderboard.songHash,
|
||||||
|
difficulty: score.leaderboard.difficulty,
|
||||||
|
maxScore: score.leaderboard.maxScore,
|
||||||
|
createdDate: score.leaderboard.createdDate,
|
||||||
|
stars: score.leaderboard.stars,
|
||||||
|
plays: score.leaderboard.plays,
|
||||||
|
coverImage: score.leaderboard.coverImage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Remove scores that are already in the database
|
// Remove scores that are already in the database
|
||||||
const player = usePlayerScoresStore.getState().get(playerId);
|
const player = useScoresaberScoresStore.getState().get(playerId);
|
||||||
if (player) {
|
if (player) {
|
||||||
scores = scores.filter(
|
scores = scores.filter(
|
||||||
(score) =>
|
(score) =>
|
||||||
@ -145,7 +175,7 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
updatePlayerScores: async () => {
|
updatePlayerScores: async () => {
|
||||||
const players = usePlayerScoresStore.getState().players;
|
const players = useScoresaberScoresStore.getState().players;
|
||||||
const friends = useSettingsStore.getState().friends;
|
const friends = useSettingsStore.getState().friends;
|
||||||
|
|
||||||
let allPlayers = new Array<ScoresaberPlayer>();
|
let allPlayers = new Array<ScoresaberPlayer>();
|
||||||
@ -159,21 +189,21 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
|
|
||||||
// add local player and friends if they don't exist
|
// add local player and friends if they don't exist
|
||||||
for (const player of allPlayers) {
|
for (const player of allPlayers) {
|
||||||
if (usePlayerScoresStore.getState().get(player.id) == undefined) {
|
if (useScoresaberScoresStore.getState().get(player.id) == undefined) {
|
||||||
toast.info(
|
toast.info(
|
||||||
`${
|
`${
|
||||||
player.id == localPlayer?.id
|
player.id == localPlayer?.id
|
||||||
? `You were`
|
? `You were`
|
||||||
: `Friend ${player.name} was`
|
: `Friend ${player.name} was`
|
||||||
} missing from the scores database, adding...`,
|
} missing from the ScoreSaber scores database, adding...`,
|
||||||
);
|
);
|
||||||
await usePlayerScoresStore.getState().addPlayer(player.id);
|
await useScoresaberScoresStore.getState().addPlayer(player.id);
|
||||||
toast.success(
|
toast.success(
|
||||||
`${
|
`${
|
||||||
player.id == useSettingsStore.getState().player?.id
|
player.id == useSettingsStore.getState().player?.id
|
||||||
? `You were`
|
? `You were`
|
||||||
: `Friend ${player.name} was`
|
: `Friend ${player.name} was`
|
||||||
} added to the scores database`,
|
} added to the ScoreSaber scores database`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,7 +211,7 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
// Skip if we refreshed the scores recently
|
// Skip if we refreshed the scores recently
|
||||||
const timeUntilRefreshMs =
|
const timeUntilRefreshMs =
|
||||||
UPDATE_INTERVAL -
|
UPDATE_INTERVAL -
|
||||||
(Date.now() - usePlayerScoresStore.getState().lastUpdated);
|
(Date.now() - useScoresaberScoresStore.getState().lastUpdated);
|
||||||
if (timeUntilRefreshMs > 0) {
|
if (timeUntilRefreshMs > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
"Waiting",
|
"Waiting",
|
||||||
@ -189,12 +219,13 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
"to refresh scores for players",
|
"to refresh scores for players",
|
||||||
);
|
);
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => usePlayerScoresStore.getState().updatePlayerScores(),
|
() => useScoresaberScoresStore.getState().updatePlayerScores(),
|
||||||
timeUntilRefreshMs,
|
timeUntilRefreshMs,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loop through all of the players and update their scores
|
||||||
for (const player of players) {
|
for (const player of players) {
|
||||||
if (player == undefined) continue;
|
if (player == undefined) continue;
|
||||||
console.log(`Updating scores for ${player.id}...`);
|
console.log(`Updating scores for ${player.id}...`);
|
||||||
@ -216,23 +247,50 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
let newScoresCount = 0;
|
let newScoresCount = 0;
|
||||||
while (search) {
|
while (search) {
|
||||||
page++;
|
page++;
|
||||||
const newScores = await fetchScores(player.id, page);
|
const newScores = await ScoreSaberAPI.fetchScores(player.id, page);
|
||||||
if (newScores == undefined) continue;
|
if (newScores == undefined) continue;
|
||||||
|
|
||||||
for (const newScore of newScores.scores) {
|
for (const score of newScores.scores) {
|
||||||
if (mostRecentScore && newScore.score.id == mostRecentScore.id) {
|
if (mostRecentScore && score.score.id == mostRecentScore.id) {
|
||||||
search = false;
|
search = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the old score
|
// remove the old score
|
||||||
const oldScoreIndex = oldScores.findIndex(
|
const oldScoreIndex = oldScores.findIndex(
|
||||||
(score) => score.score.id == newScore.score.id,
|
(score) => score.score.id == score.score.id,
|
||||||
);
|
);
|
||||||
if (oldScoreIndex != -1) {
|
if (oldScoreIndex != -1) {
|
||||||
oldScores = oldScores.splice(oldScoreIndex, 1);
|
oldScores = oldScores.splice(oldScoreIndex, 1);
|
||||||
}
|
}
|
||||||
oldScores.push(newScore);
|
oldScores.push({
|
||||||
|
score: {
|
||||||
|
id: score.score.id,
|
||||||
|
rank: score.score.rank,
|
||||||
|
baseScore: score.score.baseScore,
|
||||||
|
modifiedScore: score.score.modifiedScore,
|
||||||
|
pp: score.score.pp,
|
||||||
|
weight: score.score.weight,
|
||||||
|
modifiers: score.score.modifiers,
|
||||||
|
multiplier: score.score.multiplier,
|
||||||
|
badCuts: score.score.badCuts,
|
||||||
|
missedNotes: score.score.missedNotes,
|
||||||
|
maxCombo: score.score.maxCombo,
|
||||||
|
fullCombo: score.score.fullCombo,
|
||||||
|
hmd: score.score.hmd,
|
||||||
|
timeSet: score.score.timeSet,
|
||||||
|
},
|
||||||
|
leaderboard: {
|
||||||
|
id: score.leaderboard.id,
|
||||||
|
songHash: score.leaderboard.songHash,
|
||||||
|
difficulty: score.leaderboard.difficulty,
|
||||||
|
maxScore: score.leaderboard.maxScore,
|
||||||
|
createdDate: score.leaderboard.createdDate,
|
||||||
|
stars: score.leaderboard.stars,
|
||||||
|
plays: score.leaderboard.plays,
|
||||||
|
coverImage: score.leaderboard.coverImage,
|
||||||
|
},
|
||||||
|
});
|
||||||
newScoresCount++;
|
newScoresCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -261,7 +319,7 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "playerScores",
|
name: "scoresaberScores",
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|
||||||
@ -282,9 +340,3 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update the player scores every 30 minutes
|
|
||||||
usePlayerScoresStore.getState().updatePlayerScores();
|
|
||||||
setInterval(() => {
|
|
||||||
usePlayerScoresStore.getState().updatePlayerScores();
|
|
||||||
}, UPDATE_INTERVAL);
|
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { SortType, SortTypes } from "@/types/SortTypes";
|
import { SortType, SortTypes } from "@/types/SortTypes";
|
||||||
import { getPlayerInfo } from "@/utils/scoresaber/api";
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { createJSONStorage, persist } from "zustand/middleware";
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
@ -48,7 +48,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const friend = await getPlayerInfo(friendId);
|
const friend = await ScoreSaberAPI.getPlayerInfo(friendId);
|
||||||
if (friend == undefined || friend == null) return false;
|
if (friend == undefined || friend == null) return false;
|
||||||
|
|
||||||
set({ friends: [...friends, friend] });
|
set({ friends: [...friends, friend] });
|
||||||
|
102
src/utils/beatleader/api.ts
Normal file
102
src/utils/beatleader/api.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { BeatleaderScore } from "@/schemas/beatleader/score";
|
||||||
|
import { ssrSettings } from "@/ssrSettings";
|
||||||
|
import { FetchQueue } from "../fetchWithQueue";
|
||||||
|
import { formatString } from "../string";
|
||||||
|
|
||||||
|
// Create a fetch instance with a cache
|
||||||
|
const fetchQueue = new FetchQueue();
|
||||||
|
|
||||||
|
// Api endpoints
|
||||||
|
const API_URL = ssrSettings.proxy + "/https://api.beatleader.xyz";
|
||||||
|
const PLAYER_SCORES_URL =
|
||||||
|
API_URL + "/player/{}/scores?sortBy=date&order=0&page={}&count=100";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the players scores from the given page
|
||||||
|
*
|
||||||
|
* @param playerId the id of the player
|
||||||
|
* @param page the page to get the scores from
|
||||||
|
* @param searchType the type of search to perform
|
||||||
|
* @param limit the limit of scores to get
|
||||||
|
* @returns a list of scores
|
||||||
|
*/
|
||||||
|
async function fetchScores(
|
||||||
|
playerId: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 100,
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
scores: BeatleaderScore[];
|
||||||
|
pageInfo: {
|
||||||
|
totalScores: number;
|
||||||
|
page: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
|
if (limit > 100) {
|
||||||
|
throw new Error("Limit cannot be greater than 100");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchQueue.fetch(
|
||||||
|
formatString(PLAYER_SCORES_URL, true, playerId, page),
|
||||||
|
);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
// Check if there was an error fetching the user data
|
||||||
|
console.log(json);
|
||||||
|
|
||||||
|
const metadata = json.metadata;
|
||||||
|
return {
|
||||||
|
scores: json.data as BeatleaderScore[],
|
||||||
|
pageInfo: {
|
||||||
|
totalScores: json.totalScores,
|
||||||
|
page: json.page,
|
||||||
|
totalPages: Math.ceil(json.totalScores / metadata.itemsPerPage),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all of the players for the given player id
|
||||||
|
*
|
||||||
|
* @param playerId the id of the player
|
||||||
|
* @param searchType the type of search to perform
|
||||||
|
* @param callback a callback to call when a page is fetched
|
||||||
|
* @returns a list of scores
|
||||||
|
*/
|
||||||
|
async function fetchAllScores(
|
||||||
|
playerId: string,
|
||||||
|
callback?: (currentPage: number, totalPages: number) => void,
|
||||||
|
): Promise<BeatleaderScore[] | undefined> {
|
||||||
|
const scores = new Array();
|
||||||
|
|
||||||
|
let done = false,
|
||||||
|
page = 1;
|
||||||
|
do {
|
||||||
|
const response = await fetchScores(playerId, page);
|
||||||
|
if (response == undefined) {
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const { scores: scoresFetched } = response;
|
||||||
|
if (scoresFetched.length === 0) {
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
scores.push(...scoresFetched);
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback(page, response.pageInfo.totalPages);
|
||||||
|
}
|
||||||
|
page++;
|
||||||
|
} while (!done);
|
||||||
|
|
||||||
|
return scores as BeatleaderScore[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BeatLeaderAPI = {
|
||||||
|
fetchScores,
|
||||||
|
fetchAllScores,
|
||||||
|
};
|
@ -25,7 +25,18 @@ export class FetchQueue {
|
|||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
const retryAfter = Number(response.headers.get("retry-after")) * 1000;
|
const hasRetryAfter = response.headers.has("retry-after");
|
||||||
|
let retryAfter =
|
||||||
|
Number(
|
||||||
|
hasRetryAfter
|
||||||
|
? response.headers.get("retry-after")
|
||||||
|
: new Date(
|
||||||
|
response.headers.get("X-Rate-Limit-Reset") as string,
|
||||||
|
).getTime() / 1000,
|
||||||
|
) * 1000;
|
||||||
|
if (!retryAfter) {
|
||||||
|
retryAfter = 3_000; // default to 3 seconds if we can't get the reset time
|
||||||
|
}
|
||||||
this._queue.push(url);
|
this._queue.push(url);
|
||||||
await new Promise<void>((resolve) => setTimeout(resolve, retryAfter));
|
await new Promise<void>((resolve) => setTimeout(resolve, retryAfter));
|
||||||
return this.fetch(this._queue.shift() as string);
|
return this.fetch(this._queue.shift() as string);
|
||||||
|
@ -28,7 +28,7 @@ const SearchType = {
|
|||||||
* @param name the name to search
|
* @param name the name to search
|
||||||
* @returns a list of players
|
* @returns a list of players
|
||||||
*/
|
*/
|
||||||
export async function searchByName(
|
async function searchByName(
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<ScoresaberPlayer[] | undefined> {
|
): Promise<ScoresaberPlayer[] | undefined> {
|
||||||
const response = await fetchQueue.fetch(
|
const response = await fetchQueue.fetch(
|
||||||
@ -50,7 +50,7 @@ export async function searchByName(
|
|||||||
* @param playerId the id of the player
|
* @param playerId the id of the player
|
||||||
* @returns the player info
|
* @returns the player info
|
||||||
*/
|
*/
|
||||||
export async function getPlayerInfo(
|
async function getPlayerInfo(
|
||||||
playerId: string,
|
playerId: string,
|
||||||
): Promise<ScoresaberPlayer | undefined | null> {
|
): Promise<ScoresaberPlayer | undefined | null> {
|
||||||
const response = await fetchQueue.fetch(
|
const response = await fetchQueue.fetch(
|
||||||
@ -75,7 +75,7 @@ export async function getPlayerInfo(
|
|||||||
* @param limit the limit of scores to get
|
* @param limit the limit of scores to get
|
||||||
* @returns a list of scores
|
* @returns a list of scores
|
||||||
*/
|
*/
|
||||||
export async function fetchScores(
|
async function fetchScores(
|
||||||
playerId: string,
|
playerId: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
searchType: string = SearchType.RECENT,
|
searchType: string = SearchType.RECENT,
|
||||||
@ -92,10 +92,7 @@ export async function fetchScores(
|
|||||||
| undefined
|
| undefined
|
||||||
> {
|
> {
|
||||||
if (limit > 100) {
|
if (limit > 100) {
|
||||||
console.log(
|
throw new Error("Limit cannot be greater than 100");
|
||||||
"Scoresaber API only allows a limit of 100 scores per request, limiting to 100.",
|
|
||||||
);
|
|
||||||
limit = 100;
|
|
||||||
}
|
}
|
||||||
const response = await fetchQueue.fetch(
|
const response = await fetchQueue.fetch(
|
||||||
formatString(PLAYER_SCORES, true, playerId, limit, searchType, page),
|
formatString(PLAYER_SCORES, true, playerId, limit, searchType, page),
|
||||||
@ -127,7 +124,7 @@ export async function fetchScores(
|
|||||||
* @param callback a callback to call when a page is fetched
|
* @param callback a callback to call when a page is fetched
|
||||||
* @returns a list of scores
|
* @returns a list of scores
|
||||||
*/
|
*/
|
||||||
export async function fetchAllScores(
|
async function fetchAllScores(
|
||||||
playerId: string,
|
playerId: string,
|
||||||
searchType: string,
|
searchType: string,
|
||||||
callback?: (currentPage: number, totalPages: number) => void,
|
callback?: (currentPage: number, totalPages: number) => void,
|
||||||
@ -165,7 +162,7 @@ export async function fetchAllScores(
|
|||||||
* @param country the country to get the players from
|
* @param country the country to get the players from
|
||||||
* @returns a list of players
|
* @returns a list of players
|
||||||
*/
|
*/
|
||||||
export async function fetchTopPlayers(
|
async function fetchTopPlayers(
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
country?: string,
|
country?: string,
|
||||||
): Promise<
|
): Promise<
|
||||||
@ -201,3 +198,11 @@ export async function fetchTopPlayers(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ScoreSaberAPI = {
|
||||||
|
searchByName,
|
||||||
|
getPlayerInfo,
|
||||||
|
fetchScores,
|
||||||
|
fetchAllScores,
|
||||||
|
fetchTopPlayers,
|
||||||
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Yoinked from https://github.com/Shurdoof/pp-calculator/blob/c24b5ca452119339928831d74e6d603fb17fd5ef/src/lib/pp/calculator.ts
|
// Yoinked from https://github.com/Shurdoof/pp-calculator/blob/c24b5ca452119339928831d74e6d603fb17fd5ef/src/lib/pp/calculator.ts
|
||||||
// Thank for for this I have no fucking idea what the maths is doing but it works!
|
// Thank for for this I have no fucking idea what the maths is doing but it works!
|
||||||
|
|
||||||
import { usePlayerScoresStore } from "@/store/playerScoresStore";
|
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
||||||
|
|
||||||
export const WEIGHT_COEFFICIENT = 0.965;
|
export const WEIGHT_COEFFICIENT = 0.965;
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ function calcRawPpAtIdx(
|
|||||||
* @returns the pp boundary (+ per raw pp)
|
* @returns the pp boundary (+ per raw pp)
|
||||||
*/
|
*/
|
||||||
export function calcPpBoundary(playerId: string, expectedPp = 1) {
|
export function calcPpBoundary(playerId: string, expectedPp = 1) {
|
||||||
const rankedScores = usePlayerScoresStore
|
const rankedScores = useScoresaberScoresStore
|
||||||
.getState()
|
.getState()
|
||||||
.players.find((p) => p.id === playerId)
|
.players.find((p) => p.id === playerId)
|
||||||
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined);
|
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined);
|
||||||
@ -156,7 +156,7 @@ export function calcPpBoundary(playerId: string, expectedPp = 1) {
|
|||||||
* @returns the highest pp play
|
* @returns the highest pp play
|
||||||
*/
|
*/
|
||||||
export function getHighestPpPlay(playerId: string) {
|
export function getHighestPpPlay(playerId: string) {
|
||||||
const rankedScores = usePlayerScoresStore
|
const rankedScores = useScoresaberScoresStore
|
||||||
.getState()
|
.getState()
|
||||||
.players.find((p) => p.id === playerId)
|
.players.find((p) => p.id === playerId)
|
||||||
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined);
|
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined);
|
||||||
@ -176,7 +176,7 @@ export function getHighestPpPlay(playerId: string) {
|
|||||||
* @param limit the amount of top scores to average (default: 20)
|
* @param limit the amount of top scores to average (default: 20)
|
||||||
*/
|
*/
|
||||||
export function getAveragePp(playerId: string, limit: number = 20) {
|
export function getAveragePp(playerId: string, limit: number = 20) {
|
||||||
const rankedScores = usePlayerScoresStore
|
const rankedScores = useScoresaberScoresStore
|
||||||
.getState()
|
.getState()
|
||||||
.players.find((p) => p.id === playerId)
|
.players.find((p) => p.id === playerId)
|
||||||
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined);
|
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined);
|
||||||
|
Loading…
Reference in New Issue
Block a user