feat(overlay): add leaderboard toggle for BL and SS
All checks were successful
deploy / deploy (push) Successful in 58s
All checks were successful
deploy / deploy (push) Successful in 58s
This commit is contained in:
parent
fb2b72875f
commit
3e5f141938
@ -43,6 +43,12 @@ const nextConfig = {
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "avatars.akamai.steamstatic.com",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
BIN
public/assets/logos/beatleader.png
Normal file
BIN
public/assets/logos/beatleader.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/logos/scoresaber.png
Normal file
BIN
public/assets/logos/scoresaber.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -79,10 +79,10 @@ export default function Analytics() {
|
||||
id: "scoresaber",
|
||||
value: "ScoreSaber",
|
||||
},
|
||||
// {
|
||||
// id: "beatleader",
|
||||
// value: "BeatLeader",
|
||||
// },
|
||||
{
|
||||
id: "beatleader",
|
||||
value: "BeatLeader",
|
||||
},
|
||||
]}
|
||||
onChange={(value) => {
|
||||
settingsStore.setPlatform(value);
|
||||
|
@ -7,7 +7,8 @@ import ScoreStats from "@/components/overlay/ScoreStats";
|
||||
import SongInfo from "@/components/overlay/SongInfo";
|
||||
import { Card, CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import { HttpSiraStatus } from "@/overlay/httpSiraStatus";
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { OverlayPlayer } from "@/overlay/type/overlayPlayer";
|
||||
import { BeatLeaderAPI } from "@/utils/beatleader/api";
|
||||
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||
import { Component } from "react";
|
||||
|
||||
@ -17,7 +18,7 @@ interface OverlayProps {}
|
||||
|
||||
interface OverlayState {
|
||||
mounted: boolean;
|
||||
player: ScoresaberPlayer | undefined;
|
||||
player: OverlayPlayer | undefined;
|
||||
settings: any | undefined;
|
||||
}
|
||||
|
||||
@ -31,13 +32,44 @@ export default class Overlay extends Component<OverlayProps, OverlayState> {
|
||||
};
|
||||
}
|
||||
|
||||
updatePlayer = async (playerId: string) => {
|
||||
updatePlayer = async (
|
||||
playerId: string,
|
||||
leaderboard: "scoresaber" | "beatleader" = "scoresaber",
|
||||
) => {
|
||||
console.log(`Updating player stats for ${playerId}`);
|
||||
const player = await ScoreSaberAPI.fetchPlayerData(playerId);
|
||||
if (!player) {
|
||||
return;
|
||||
if (leaderboard == "scoresaber") {
|
||||
const player = await ScoreSaberAPI.fetchPlayerData(playerId);
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
player: {
|
||||
id: player.id,
|
||||
profilePicture: player.profilePicture,
|
||||
country: player.country,
|
||||
pp: player.pp,
|
||||
rank: player.rank,
|
||||
countryRank: player.countryRank,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (leaderboard == "beatleader") {
|
||||
const player = await BeatLeaderAPI.fetchPlayerData(playerId);
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
player: {
|
||||
id: player.id,
|
||||
profilePicture: player.avatar,
|
||||
country: player.country,
|
||||
pp: player.pp,
|
||||
rank: player.rank,
|
||||
countryRank: player.countryRank,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.setState({ player });
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -60,9 +92,9 @@ export default class Overlay extends Component<OverlayProps, OverlayState> {
|
||||
this.setState({ settings: settings });
|
||||
|
||||
if (settings.settings.showPlayerStats) {
|
||||
this.updatePlayer(settings.accountId);
|
||||
this.updatePlayer(settings.accountId, settings.platform);
|
||||
setInterval(() => {
|
||||
this.updatePlayer(settings.accountId);
|
||||
this.updatePlayer(settings.accountId, settings.platform);
|
||||
}, UPDATE_INTERVAL);
|
||||
}
|
||||
}
|
||||
@ -113,7 +145,7 @@ export default class Overlay extends Component<OverlayProps, OverlayState> {
|
||||
<main>
|
||||
<div>
|
||||
{this.state.settings.settings.showPlayerStats && player && (
|
||||
<PlayerStats player={player} />
|
||||
<PlayerStats player={player} settings={this.state.settings} />
|
||||
)}
|
||||
{this.state.settings.settings.showScoreStats && <ScoreStats />}
|
||||
</div>
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { OverlayPlayer } from "@/overlay/type/overlayPlayer";
|
||||
import { formatNumber } from "@/utils/numberUtils";
|
||||
import { GlobeAltIcon } from "@heroicons/react/20/solid";
|
||||
import Image from "next/image";
|
||||
import CountyFlag from "../CountryFlag";
|
||||
|
||||
type PlayerStatsProps = {
|
||||
player: ScoresaberPlayer;
|
||||
player: OverlayPlayer;
|
||||
settings: any;
|
||||
};
|
||||
|
||||
export default function PlayerStats({ player }: PlayerStatsProps) {
|
||||
const leaderboardImages: Record<string, string> = {
|
||||
scoresaber: "/assets/logos/scoresaber.png",
|
||||
beatleader: "/assets/logos/beatleader.png",
|
||||
};
|
||||
|
||||
export default function PlayerStats({ player, settings }: PlayerStatsProps) {
|
||||
return (
|
||||
<div className="flex gap-2 p-2">
|
||||
<Image
|
||||
@ -19,7 +25,15 @@ export default function PlayerStats({ player }: PlayerStatsProps) {
|
||||
height={180}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{formatNumber(player.pp, 2)}pp</p>
|
||||
<div className="flex gap-2">
|
||||
<Image
|
||||
alt="Leaderboard logo"
|
||||
src={leaderboardImages[settings.platform]}
|
||||
width={36}
|
||||
height={36}
|
||||
/>
|
||||
<p className="text-3xl font-bold">{formatNumber(player.pp, 2)}pp</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<GlobeAltIcon width={25} height={25} />
|
||||
<p className="text-3xl">#{formatNumber(player.rank)}</p>
|
||||
|
8
src/overlay/type/overlayPlayer.ts
Normal file
8
src/overlay/type/overlayPlayer.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export type OverlayPlayer = {
|
||||
id: string;
|
||||
country: string;
|
||||
profilePicture: string;
|
||||
pp: number;
|
||||
rank: number;
|
||||
countryRank: number;
|
||||
};
|
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;
|
||||
};
|
9
src/schemas/beatleader/player.ts
Normal file
9
src/schemas/beatleader/player.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export type BeatLeaderPlayer = {
|
||||
id: string;
|
||||
country: string;
|
||||
avatar: string;
|
||||
pp: number;
|
||||
rank: number;
|
||||
countryRank: number;
|
||||
// todo: finish this
|
||||
};
|
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; // ??
|
||||
};
|
126
src/utils/beatleader/api.ts
Normal file
126
src/utils/beatleader/api.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { BeatLeaderPlayer } from "@/schemas/beatleader/player";
|
||||
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";
|
||||
const PLAYER_URL = API_URL + "/player/{}?stats=false";
|
||||
|
||||
/**
|
||||
* Get the player from the given player id
|
||||
*
|
||||
* @param playerId the id of the player
|
||||
* @param searchType the type of search to perform
|
||||
* @returns the player
|
||||
*/
|
||||
async function fetchPlayerData(
|
||||
playerId: string,
|
||||
): Promise<BeatLeaderPlayer | undefined> {
|
||||
const response = await fetchQueue.fetch(
|
||||
formatString(PLAYER_URL, true, playerId),
|
||||
);
|
||||
const json = await response.json();
|
||||
|
||||
// Check if there was an error fetching the user data
|
||||
console.log(json);
|
||||
|
||||
return json as BeatLeaderPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
fetchPlayerData,
|
||||
fetchScores,
|
||||
fetchAllScores,
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user