fix crying
This commit is contained in:
parent
1d9433ef02
commit
cd1f010698
@ -6,12 +6,13 @@ import { getDifficultyFromScoreSaberDifficulty } from "@ssr/common/utils/scoresa
|
|||||||
import { StarIcon } from "../../components/star-icon";
|
import { StarIcon } from "../../components/star-icon";
|
||||||
import { GlobeIcon } from "../../components/globe-icon";
|
import { GlobeIcon } from "../../components/globe-icon";
|
||||||
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player";
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||||
import { Jimp } from "jimp";
|
import { Jimp } from "jimp";
|
||||||
import { extractColors } from "extract-colors";
|
import { extractColors } from "extract-colors";
|
||||||
import { Config } from "@ssr/common/config";
|
import { Config } from "@ssr/common/config";
|
||||||
import { fetchWithCache } from "../common/cache.util";
|
import { fetchWithCache } from "../common/cache.util";
|
||||||
import { SSRCache } from "@ssr/common/cache";
|
import { SSRCache } from "@ssr/common/cache";
|
||||||
|
import { getScoreSaberPlayerFromToken } from "@ssr/common/token-creators";
|
||||||
|
|
||||||
const cache = new SSRCache({
|
const cache = new SSRCache({
|
||||||
ttl: 1000 * 60 * 60, // 1 hour
|
ttl: 1000 * 60 * 60, // 1 hour
|
||||||
|
@ -5,9 +5,9 @@ import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response";
|
|||||||
import Leaderboard from "@ssr/common/leaderboard/leaderboard";
|
import Leaderboard from "@ssr/common/leaderboard/leaderboard";
|
||||||
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
import { NotFoundError } from "elysia";
|
import { NotFoundError } from "elysia";
|
||||||
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
|
||||||
import BeatSaverService from "./beatsaver.service";
|
import BeatSaverService from "./beatsaver.service";
|
||||||
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
||||||
|
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/token-creators";
|
||||||
|
|
||||||
const leaderboardCache = new SSRCache({
|
const leaderboardCache = new SSRCache({
|
||||||
ttl: 1000 * 60 * 60 * 24,
|
ttl: 1000 * 60 * 60 * 24,
|
||||||
|
@ -4,9 +4,6 @@ import { isProduction } from "@ssr/common/utils/utils";
|
|||||||
import { Metadata } from "@ssr/common/types/metadata";
|
import { Metadata } from "@ssr/common/types/metadata";
|
||||||
import { NotFoundError } from "elysia";
|
import { NotFoundError } from "elysia";
|
||||||
import BeatSaverService from "./beatsaver.service";
|
import BeatSaverService from "./beatsaver.service";
|
||||||
import ScoreSaberLeaderboard, {
|
|
||||||
getScoreSaberLeaderboardFromToken,
|
|
||||||
} from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
|
||||||
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||||
import { ScoreSort } from "@ssr/common/score/score-sort";
|
import { ScoreSort } from "@ssr/common/score/score-sort";
|
||||||
import { Leaderboards } from "@ssr/common/leaderboard";
|
import { Leaderboards } from "@ssr/common/leaderboard";
|
||||||
@ -28,8 +25,10 @@ import {
|
|||||||
AdditionalScoreDataModel,
|
AdditionalScoreDataModel,
|
||||||
} from "@ssr/common/model/additional-score-data/additional-score-data";
|
} from "@ssr/common/model/additional-score-data/additional-score-data";
|
||||||
import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement";
|
import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement";
|
||||||
import { getScoreSaberScoreFromToken, ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score";
|
|
||||||
import { ScoreType } from "@ssr/common/model/score/score";
|
import { ScoreType } from "@ssr/common/model/score/score";
|
||||||
|
import { getScoreSaberLeaderboardFromToken, getScoreSaberScoreFromToken } from "@ssr/common/token-creators";
|
||||||
|
import { ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||||
|
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
||||||
|
|
||||||
const playerScoresCache = new SSRCache({
|
const playerScoresCache = new SSRCache({
|
||||||
ttl: 1000 * 60, // 1 minute
|
ttl: 1000 * 60, // 1 minute
|
||||||
|
@ -32,54 +32,3 @@ export default interface ScoreSaberLeaderboard extends Leaderboard {
|
|||||||
*/
|
*/
|
||||||
readonly status: LeaderboardStatus;
|
readonly status: LeaderboardStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a {@link ScoreSaberLeaderboardToken} into a {@link ScoreSaberLeaderboard}.
|
|
||||||
*
|
|
||||||
* @param token the token to parse
|
|
||||||
*/
|
|
||||||
export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardToken): ScoreSaberLeaderboard {
|
|
||||||
const difficulty: LeaderboardDifficulty = {
|
|
||||||
leaderboardId: token.difficulty.leaderboardId,
|
|
||||||
difficulty: getDifficultyFromScoreSaberDifficulty(token.difficulty.difficulty),
|
|
||||||
characteristic: token.difficulty.gameMode.replace("Solo", "") as MapCharacteristic,
|
|
||||||
difficultyRaw: token.difficulty.difficultyRaw,
|
|
||||||
};
|
|
||||||
|
|
||||||
let status: LeaderboardStatus = "Unranked";
|
|
||||||
if (token.qualified) {
|
|
||||||
status = "Qualified";
|
|
||||||
} else if (token.ranked) {
|
|
||||||
status = "Ranked";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: token.id,
|
|
||||||
songHash: token.songHash.toUpperCase(),
|
|
||||||
songName: token.songName,
|
|
||||||
songSubName: token.songSubName,
|
|
||||||
songAuthorName: token.songAuthorName,
|
|
||||||
levelAuthorName: token.levelAuthorName,
|
|
||||||
difficulty: difficulty,
|
|
||||||
difficulties:
|
|
||||||
token.difficulties != undefined && token.difficulties.length > 0
|
|
||||||
? token.difficulties.map(difficulty => {
|
|
||||||
return {
|
|
||||||
leaderboardId: difficulty.leaderboardId,
|
|
||||||
difficulty: getDifficultyFromScoreSaberDifficulty(difficulty.difficulty),
|
|
||||||
characteristic: difficulty.gameMode.replace("Solo", "") as MapCharacteristic,
|
|
||||||
difficultyRaw: difficulty.difficultyRaw,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [difficulty],
|
|
||||||
maxScore: token.maxScore,
|
|
||||||
ranked: token.ranked,
|
|
||||||
songArt: token.coverImage,
|
|
||||||
timestamp: parseDate(token.createdDate),
|
|
||||||
stars: token.stars,
|
|
||||||
plays: token.plays,
|
|
||||||
dailyPlays: token.dailyPlays,
|
|
||||||
qualified: token.qualified,
|
|
||||||
status: status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -71,53 +71,6 @@ class ScoreSaberScorePublic extends ScoreSaberScoreInternal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ScoreSaberScore = InstanceType<typeof ScoreSaberScorePublic>;
|
export type ScoreSaberScore = InstanceType<typeof ScoreSaberScorePublic>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a {@link ScoreSaberScore} from a {@link ScoreSaberScoreToken}.
|
|
||||||
*
|
|
||||||
* @param token the token to convert
|
|
||||||
* @param playerId the id of the player who set the score
|
|
||||||
* @param leaderboard the leaderboard the score was set on
|
|
||||||
*/
|
|
||||||
export function getScoreSaberScoreFromToken(
|
|
||||||
token: ScoreSaberScoreToken,
|
|
||||||
leaderboard: ScoreSaberLeaderboard,
|
|
||||||
playerId?: string
|
|
||||||
): ScoreSaberScore {
|
|
||||||
const modifiers: Modifier[] =
|
|
||||||
token.modifiers == undefined || token.modifiers === ""
|
|
||||||
? []
|
|
||||||
: token.modifiers.split(",").map(mod => {
|
|
||||||
mod = mod.toUpperCase();
|
|
||||||
const modifier = Modifier[mod as keyof typeof Modifier];
|
|
||||||
if (modifier === undefined) {
|
|
||||||
throw new Error(`Unknown modifier: ${mod}`);
|
|
||||||
}
|
|
||||||
return modifier;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
playerId: playerId || token.leaderboardPlayerInfo.id,
|
|
||||||
leaderboardId: leaderboard.id,
|
|
||||||
difficulty: leaderboard.difficulty.difficulty,
|
|
||||||
characteristic: leaderboard.difficulty.characteristic,
|
|
||||||
score: token.baseScore,
|
|
||||||
accuracy: (token.baseScore / leaderboard.maxScore) * 100,
|
|
||||||
rank: token.rank,
|
|
||||||
modifiers: modifiers,
|
|
||||||
misses: token.missedNotes + token.badCuts,
|
|
||||||
missedNotes: token.missedNotes,
|
|
||||||
badCuts: token.badCuts,
|
|
||||||
fullCombo: token.fullCombo,
|
|
||||||
timestamp: new Date(token.timeSet),
|
|
||||||
scoreId: token.id,
|
|
||||||
pp: token.pp,
|
|
||||||
weight: token.weight,
|
|
||||||
maxCombo: token.maxCombo,
|
|
||||||
playerInfo: token.leaderboardPlayerInfo,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ScoreSaberScoreDocument = ScoreSaberScore & Document;
|
export type ScoreSaberScoreDocument = ScoreSaberScore & Document;
|
||||||
export const ScoreSaberScoreModel: ReturnModelType<typeof ScoreSaberScoreInternal> =
|
export const ScoreSaberScoreModel: ReturnModelType<typeof ScoreSaberScoreInternal> =
|
||||||
getModelForClass(ScoreSaberScoreInternal);
|
getModelForClass(ScoreSaberScoreInternal);
|
||||||
|
@ -73,223 +73,6 @@ export default interface ScoreSaberPlayer extends Player {
|
|||||||
isBeingTracked?: boolean;
|
isBeingTracked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the ScoreSaber Player from an {@link ScoreSaberPlayerToken}.
|
|
||||||
*
|
|
||||||
* @param token the player token
|
|
||||||
* @param playerIdCookie the id of the claimed player
|
|
||||||
*/
|
|
||||||
export async function getScoreSaberPlayerFromToken(
|
|
||||||
token: ScoreSaberPlayerToken,
|
|
||||||
playerIdCookie?: string
|
|
||||||
): Promise<ScoreSaberPlayer> {
|
|
||||||
const bio: ScoreSaberBio = {
|
|
||||||
lines: token.bio?.split("\n") || [],
|
|
||||||
linesStripped: token.bio?.replace(/<[^>]+>/g, "")?.split("\n") || [],
|
|
||||||
};
|
|
||||||
const badges: ScoreSaberBadge[] =
|
|
||||||
token.badges?.map(badge => {
|
|
||||||
return {
|
|
||||||
url: badge.image,
|
|
||||||
description: badge.description,
|
|
||||||
};
|
|
||||||
}) || [];
|
|
||||||
|
|
||||||
let isBeingTracked = false;
|
|
||||||
const todayDate = formatDateMinimal(getMidnightAlignedDate(new Date()));
|
|
||||||
let statisticHistory: { [key: string]: PlayerHistory } = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { statistics: history } = await ky
|
|
||||||
.get<{
|
|
||||||
statistics: { [key: string]: PlayerHistory };
|
|
||||||
}>(
|
|
||||||
`${Config.apiUrl}/player/history/${token.id}/50/${playerIdCookie && playerIdCookie == token.id ? "?createIfMissing=true" : ""}`
|
|
||||||
)
|
|
||||||
.json();
|
|
||||||
if (history) {
|
|
||||||
// Use the latest data for today
|
|
||||||
history[todayDate] = {
|
|
||||||
...history[todayDate],
|
|
||||||
rank: token.rank,
|
|
||||||
countryRank: token.countryRank,
|
|
||||||
pp: token.pp,
|
|
||||||
replaysWatched: token.scoreStats.replaysWatched,
|
|
||||||
accuracy: {
|
|
||||||
...history[todayDate]?.accuracy,
|
|
||||||
averageRankedAccuracy: token.scoreStats.averageRankedAccuracy,
|
|
||||||
},
|
|
||||||
scores: {
|
|
||||||
...history[todayDate]?.scores,
|
|
||||||
totalScores: token.scoreStats.totalPlayCount,
|
|
||||||
totalRankedScores: token.scoreStats.rankedPlayCount,
|
|
||||||
},
|
|
||||||
score: {
|
|
||||||
...history[todayDate]?.score,
|
|
||||||
totalScore: token.scoreStats.totalScore,
|
|
||||||
totalRankedScore: token.scoreStats.totalRankedScore,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
isBeingTracked = true;
|
|
||||||
}
|
|
||||||
statisticHistory = history;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
const playerRankHistory = token.histories.split(",").map(value => {
|
|
||||||
return parseInt(value);
|
|
||||||
});
|
|
||||||
playerRankHistory.push(token.rank);
|
|
||||||
|
|
||||||
let missingDays = 0;
|
|
||||||
let daysAgo = 0; // Start from current day
|
|
||||||
for (let i = playerRankHistory.length - 1; i >= 0; i--) {
|
|
||||||
const rank = playerRankHistory[i];
|
|
||||||
if (rank == 999_999) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = getMidnightAlignedDate(getDaysAgoDate(daysAgo));
|
|
||||||
daysAgo += 1;
|
|
||||||
|
|
||||||
const dateKey = formatDateMinimal(date);
|
|
||||||
if (!statisticHistory[dateKey] || statisticHistory[dateKey].rank == undefined) {
|
|
||||||
missingDays += 1;
|
|
||||||
statisticHistory[dateKey] = {
|
|
||||||
...statisticHistory[dateKey],
|
|
||||||
rank: rank,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingDays > 0 && missingDays != playerRankHistory.length) {
|
|
||||||
console.log(
|
|
||||||
`Player has ${missingDays} missing day${missingDays > 1 ? "s" : ""}, filling in with fallback history...`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the fallback history
|
|
||||||
statisticHistory = Object.entries(statisticHistory)
|
|
||||||
.sort((a, b) => Date.parse(b[0]) - Date.parse(a[0]))
|
|
||||||
.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the change in the given stat
|
|
||||||
*
|
|
||||||
* @param statType the stat to check
|
|
||||||
* @param isNegativeChange whether to multiply the change by 1 or -1
|
|
||||||
* @param daysAgo the amount of days ago to get the stat for
|
|
||||||
* @return the change
|
|
||||||
*/
|
|
||||||
const getStatisticChange = (statType: string, isNegativeChange: boolean, daysAgo: number = 1): number | undefined => {
|
|
||||||
const todayStats = statisticHistory[todayDate];
|
|
||||||
let otherDate: Date | undefined;
|
|
||||||
|
|
||||||
if (daysAgo === 1) {
|
|
||||||
otherDate = getMidnightAlignedDate(getDaysAgoDate(1)); // Yesterday
|
|
||||||
} else {
|
|
||||||
const targetDate = getDaysAgoDate(daysAgo);
|
|
||||||
|
|
||||||
// Filter available dates to find the closest one to the target
|
|
||||||
const availableDates = Object.keys(statisticHistory)
|
|
||||||
.map(dateKey => new Date(dateKey))
|
|
||||||
.filter(date => {
|
|
||||||
// Convert date back to the correct format for statisticHistory lookup
|
|
||||||
const formattedDate = formatDateMinimal(date);
|
|
||||||
const statsForDate = statisticHistory[formattedDate];
|
|
||||||
const hasStat = statsForDate && statType in statsForDate;
|
|
||||||
|
|
||||||
// Only consider past dates with the required statType
|
|
||||||
const isPast = date.getTime() < new Date().getTime();
|
|
||||||
return hasStat && isPast;
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no valid dates are found, return undefined
|
|
||||||
if (availableDates.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the closest date from the filtered available dates
|
|
||||||
otherDate = availableDates.reduce((closestDate, currentDate) => {
|
|
||||||
const currentDiff = Math.abs(currentDate.getTime() - targetDate.getTime());
|
|
||||||
const closestDiff = Math.abs(closestDate.getTime() - targetDate.getTime());
|
|
||||||
return currentDiff < closestDiff ? currentDate : closestDate;
|
|
||||||
}, availableDates[0]); // Start with the first available date
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure todayStats exists and contains the statType
|
|
||||||
if (!todayStats || !getValueFromHistory(todayStats, statType)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const otherStats = statisticHistory[formatDateMinimal(otherDate)]; // This is now validated
|
|
||||||
|
|
||||||
// Ensure otherStats exists and contains the statType
|
|
||||||
if (!otherStats || !getValueFromHistory(otherStats, statType)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statToday = getValueFromHistory(todayStats, statType);
|
|
||||||
const statOther = getValueFromHistory(otherStats, statType);
|
|
||||||
|
|
||||||
if (statToday === undefined || statOther === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return (statToday - statOther) * (!isNegativeChange ? 1 : -1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatisticChanges = (daysAgo: number): PlayerHistory => {
|
|
||||||
return {
|
|
||||||
rank: getStatisticChange("rank", true, daysAgo),
|
|
||||||
countryRank: getStatisticChange("countryRank", true, daysAgo),
|
|
||||||
pp: getStatisticChange("pp", false, daysAgo),
|
|
||||||
replaysWatched: getStatisticChange("replaysWatched", false, daysAgo),
|
|
||||||
accuracy: {
|
|
||||||
averageRankedAccuracy: getStatisticChange("accuracy.averageRankedAccuracy", false, daysAgo),
|
|
||||||
},
|
|
||||||
score: {
|
|
||||||
totalScore: getStatisticChange("score.totalScore", false, daysAgo),
|
|
||||||
totalRankedScore: getStatisticChange("score.totalRankedScore", false, daysAgo),
|
|
||||||
},
|
|
||||||
scores: {
|
|
||||||
totalScores: getStatisticChange("scores.totalScores", false, daysAgo),
|
|
||||||
totalRankedScores: getStatisticChange("scores.totalRankedScores", false, daysAgo),
|
|
||||||
rankedScores: getStatisticChange("scores.rankedScores", false, daysAgo),
|
|
||||||
unrankedScores: getStatisticChange("scores.unrankedScores", false, daysAgo),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: token.id,
|
|
||||||
name: token.name,
|
|
||||||
avatar: token.profilePicture,
|
|
||||||
country: token.country,
|
|
||||||
rank: token.rank,
|
|
||||||
countryRank: token.countryRank,
|
|
||||||
joinedDate: new Date(token.firstSeen),
|
|
||||||
bio: bio,
|
|
||||||
pp: token.pp,
|
|
||||||
statisticChange: {
|
|
||||||
daily: getStatisticChanges(1),
|
|
||||||
weekly: getStatisticChanges(7),
|
|
||||||
monthly: getStatisticChanges(30),
|
|
||||||
},
|
|
||||||
role: token.role == null ? undefined : token.role,
|
|
||||||
badges: badges,
|
|
||||||
statisticHistory: statisticHistory,
|
|
||||||
statistics: token.scoreStats,
|
|
||||||
rankPages: {
|
|
||||||
global: getPageFromRank(token.rank, 50),
|
|
||||||
country: getPageFromRank(token.countryRank, 50),
|
|
||||||
},
|
|
||||||
permissions: token.permissions,
|
|
||||||
banned: token.banned,
|
|
||||||
inactive: token.inactive,
|
|
||||||
isBeingTracked: isBeingTracked,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A bio of a player.
|
* A bio of a player.
|
||||||
*/
|
*/
|
||||||
|
331
projects/common/src/token-creators.ts
Normal file
331
projects/common/src/token-creators.ts
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
import ScoreSaberLeaderboard from "./leaderboard/impl/scoresaber-leaderboard";
|
||||||
|
import ScoreSaberLeaderboardToken from "./types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
|
import LeaderboardDifficulty from "./leaderboard/leaderboard-difficulty";
|
||||||
|
import { getDifficultyFromScoreSaberDifficulty } from "./utils/scoresaber-utils";
|
||||||
|
import { MapCharacteristic } from "./types/map-characteristic";
|
||||||
|
import { LeaderboardStatus } from "./leaderboard/leaderboard-status";
|
||||||
|
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate, parseDate } from "./utils/time-utils";
|
||||||
|
import ScoreSaberPlayerToken from "./types/token/scoresaber/score-saber-player-token";
|
||||||
|
import ScoreSaberPlayer, { ScoreSaberBadge, ScoreSaberBio } from "./player/impl/scoresaber-player";
|
||||||
|
import { PlayerHistory } from "./player/player-history";
|
||||||
|
import ky from "ky";
|
||||||
|
import { Config } from "./config";
|
||||||
|
import { getValueFromHistory } from "./utils/player-utils";
|
||||||
|
import { getPageFromRank } from "./utils/utils";
|
||||||
|
import ScoreSaberScoreToken from "./types/token/scoresaber/score-saber-score-token";
|
||||||
|
import { ScoreSaberScore } from "./model/score/impl/scoresaber-score";
|
||||||
|
import { Modifier } from "./score/modifier";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a {@link ScoreSaberLeaderboardToken} into a {@link ScoreSaberLeaderboard}.
|
||||||
|
*
|
||||||
|
* @param token the token to parse
|
||||||
|
*/
|
||||||
|
export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardToken): ScoreSaberLeaderboard {
|
||||||
|
const difficulty: LeaderboardDifficulty = {
|
||||||
|
leaderboardId: token.difficulty.leaderboardId,
|
||||||
|
difficulty: getDifficultyFromScoreSaberDifficulty(token.difficulty.difficulty),
|
||||||
|
characteristic: token.difficulty.gameMode.replace("Solo", "") as MapCharacteristic,
|
||||||
|
difficultyRaw: token.difficulty.difficultyRaw,
|
||||||
|
};
|
||||||
|
|
||||||
|
let status: LeaderboardStatus = "Unranked";
|
||||||
|
if (token.qualified) {
|
||||||
|
status = "Qualified";
|
||||||
|
} else if (token.ranked) {
|
||||||
|
status = "Ranked";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: token.id,
|
||||||
|
songHash: token.songHash.toUpperCase(),
|
||||||
|
songName: token.songName,
|
||||||
|
songSubName: token.songSubName,
|
||||||
|
songAuthorName: token.songAuthorName,
|
||||||
|
levelAuthorName: token.levelAuthorName,
|
||||||
|
difficulty: difficulty,
|
||||||
|
difficulties:
|
||||||
|
token.difficulties != undefined && token.difficulties.length > 0
|
||||||
|
? token.difficulties.map(difficulty => {
|
||||||
|
return {
|
||||||
|
leaderboardId: difficulty.leaderboardId,
|
||||||
|
difficulty: getDifficultyFromScoreSaberDifficulty(difficulty.difficulty),
|
||||||
|
characteristic: difficulty.gameMode.replace("Solo", "") as MapCharacteristic,
|
||||||
|
difficultyRaw: difficulty.difficultyRaw,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [difficulty],
|
||||||
|
maxScore: token.maxScore,
|
||||||
|
ranked: token.ranked,
|
||||||
|
songArt: token.coverImage,
|
||||||
|
timestamp: parseDate(token.createdDate),
|
||||||
|
stars: token.stars,
|
||||||
|
plays: token.plays,
|
||||||
|
dailyPlays: token.dailyPlays,
|
||||||
|
qualified: token.qualified,
|
||||||
|
status: status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link ScoreSaberScore} from a {@link ScoreSaberScoreToken}.
|
||||||
|
*
|
||||||
|
* @param token the token to convert
|
||||||
|
* @param playerId the id of the player who set the score
|
||||||
|
* @param leaderboard the leaderboard the score was set on
|
||||||
|
*/
|
||||||
|
export function getScoreSaberScoreFromToken(
|
||||||
|
token: ScoreSaberScoreToken,
|
||||||
|
leaderboard: ScoreSaberLeaderboard,
|
||||||
|
playerId?: string
|
||||||
|
): ScoreSaberScore {
|
||||||
|
const modifiers: Modifier[] =
|
||||||
|
token.modifiers == undefined || token.modifiers === ""
|
||||||
|
? []
|
||||||
|
: token.modifiers.split(",").map(mod => {
|
||||||
|
mod = mod.toUpperCase();
|
||||||
|
const modifier = Modifier[mod as keyof typeof Modifier];
|
||||||
|
if (modifier === undefined) {
|
||||||
|
throw new Error(`Unknown modifier: ${mod}`);
|
||||||
|
}
|
||||||
|
return modifier;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
playerId: playerId || token.leaderboardPlayerInfo.id,
|
||||||
|
leaderboardId: leaderboard.id,
|
||||||
|
difficulty: leaderboard.difficulty.difficulty,
|
||||||
|
characteristic: leaderboard.difficulty.characteristic,
|
||||||
|
score: token.baseScore,
|
||||||
|
accuracy: (token.baseScore / leaderboard.maxScore) * 100,
|
||||||
|
rank: token.rank,
|
||||||
|
modifiers: modifiers,
|
||||||
|
misses: token.missedNotes + token.badCuts,
|
||||||
|
missedNotes: token.missedNotes,
|
||||||
|
badCuts: token.badCuts,
|
||||||
|
fullCombo: token.fullCombo,
|
||||||
|
timestamp: new Date(token.timeSet),
|
||||||
|
scoreId: token.id,
|
||||||
|
pp: token.pp,
|
||||||
|
weight: token.weight,
|
||||||
|
maxCombo: token.maxCombo,
|
||||||
|
playerInfo: token.leaderboardPlayerInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the ScoreSaber Player from an {@link ScoreSaberPlayerToken}.
|
||||||
|
*
|
||||||
|
* @param token the player token
|
||||||
|
* @param playerIdCookie the id of the claimed player
|
||||||
|
*/
|
||||||
|
export async function getScoreSaberPlayerFromToken(
|
||||||
|
token: ScoreSaberPlayerToken,
|
||||||
|
playerIdCookie?: string
|
||||||
|
): Promise<ScoreSaberPlayer> {
|
||||||
|
const bio: ScoreSaberBio = {
|
||||||
|
lines: token.bio?.split("\n") || [],
|
||||||
|
linesStripped: token.bio?.replace(/<[^>]+>/g, "")?.split("\n") || [], // strips html tags
|
||||||
|
};
|
||||||
|
const badges: ScoreSaberBadge[] =
|
||||||
|
token.badges?.map(badge => {
|
||||||
|
return {
|
||||||
|
url: badge.image,
|
||||||
|
description: badge.description,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
let isBeingTracked = false;
|
||||||
|
const todayDate = formatDateMinimal(getMidnightAlignedDate(new Date()));
|
||||||
|
let statisticHistory: { [key: string]: PlayerHistory } = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { statistics: history } = await ky
|
||||||
|
.get<{
|
||||||
|
statistics: { [key: string]: PlayerHistory };
|
||||||
|
}>(
|
||||||
|
`${Config.apiUrl}/player/history/${token.id}/50/${playerIdCookie && playerIdCookie == token.id ? "?createIfMissing=true" : ""}`
|
||||||
|
)
|
||||||
|
.json();
|
||||||
|
if (history) {
|
||||||
|
// Use the latest data for today
|
||||||
|
history[todayDate] = {
|
||||||
|
...history[todayDate],
|
||||||
|
rank: token.rank,
|
||||||
|
countryRank: token.countryRank,
|
||||||
|
pp: token.pp,
|
||||||
|
replaysWatched: token.scoreStats.replaysWatched,
|
||||||
|
accuracy: {
|
||||||
|
...history[todayDate]?.accuracy,
|
||||||
|
averageRankedAccuracy: token.scoreStats.averageRankedAccuracy,
|
||||||
|
},
|
||||||
|
scores: {
|
||||||
|
...history[todayDate]?.scores,
|
||||||
|
totalScores: token.scoreStats.totalPlayCount,
|
||||||
|
totalRankedScores: token.scoreStats.rankedPlayCount,
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
...history[todayDate]?.score,
|
||||||
|
totalScore: token.scoreStats.totalScore,
|
||||||
|
totalRankedScore: token.scoreStats.totalRankedScore,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
isBeingTracked = true;
|
||||||
|
}
|
||||||
|
statisticHistory = history;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const playerRankHistory = token.histories.split(",").map(value => {
|
||||||
|
return parseInt(value);
|
||||||
|
});
|
||||||
|
playerRankHistory.push(token.rank);
|
||||||
|
|
||||||
|
let missingDays = 0;
|
||||||
|
let daysAgo = 0; // Start from current day
|
||||||
|
for (let i = playerRankHistory.length - 1; i >= 0; i--) {
|
||||||
|
const rank = playerRankHistory[i];
|
||||||
|
if (rank == 999_999) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = getMidnightAlignedDate(getDaysAgoDate(daysAgo));
|
||||||
|
daysAgo += 1;
|
||||||
|
|
||||||
|
const dateKey = formatDateMinimal(date);
|
||||||
|
if (!statisticHistory[dateKey] || statisticHistory[dateKey].rank == undefined) {
|
||||||
|
missingDays += 1;
|
||||||
|
statisticHistory[dateKey] = {
|
||||||
|
...statisticHistory[dateKey],
|
||||||
|
rank: rank,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingDays > 0 && missingDays != playerRankHistory.length) {
|
||||||
|
console.log(
|
||||||
|
`Player has ${missingDays} missing day${missingDays > 1 ? "s" : ""}, filling in with fallback history...`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the fallback history
|
||||||
|
statisticHistory = Object.entries(statisticHistory)
|
||||||
|
.sort((a, b) => Date.parse(b[0]) - Date.parse(a[0]))
|
||||||
|
.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the change in the given stat
|
||||||
|
*
|
||||||
|
* @param statType the stat to check
|
||||||
|
* @param isNegativeChange whether to multiply the change by 1 or -1
|
||||||
|
* @param daysAgo the amount of days ago to get the stat for
|
||||||
|
* @return the change
|
||||||
|
*/
|
||||||
|
const getStatisticChange = (statType: string, isNegativeChange: boolean, daysAgo: number = 1): number | undefined => {
|
||||||
|
const todayStats = statisticHistory[todayDate];
|
||||||
|
let otherDate: Date | undefined;
|
||||||
|
|
||||||
|
if (daysAgo === 1) {
|
||||||
|
otherDate = getMidnightAlignedDate(getDaysAgoDate(1)); // Yesterday
|
||||||
|
} else {
|
||||||
|
const targetDate = getDaysAgoDate(daysAgo);
|
||||||
|
|
||||||
|
// Filter available dates to find the closest one to the target
|
||||||
|
const availableDates = Object.keys(statisticHistory)
|
||||||
|
.map(dateKey => new Date(dateKey))
|
||||||
|
.filter(date => {
|
||||||
|
// Convert date back to the correct format for statisticHistory lookup
|
||||||
|
const formattedDate = formatDateMinimal(date);
|
||||||
|
const statsForDate = statisticHistory[formattedDate];
|
||||||
|
const hasStat = statsForDate && statType in statsForDate;
|
||||||
|
|
||||||
|
// Only consider past dates with the required statType
|
||||||
|
const isPast = date.getTime() < new Date().getTime();
|
||||||
|
return hasStat && isPast;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no valid dates are found, return undefined
|
||||||
|
if (availableDates.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the closest date from the filtered available dates
|
||||||
|
otherDate = availableDates.reduce((closestDate, currentDate) => {
|
||||||
|
const currentDiff = Math.abs(currentDate.getTime() - targetDate.getTime());
|
||||||
|
const closestDiff = Math.abs(closestDate.getTime() - targetDate.getTime());
|
||||||
|
return currentDiff < closestDiff ? currentDate : closestDate;
|
||||||
|
}, availableDates[0]); // Start with the first available date
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure todayStats exists and contains the statType
|
||||||
|
if (!todayStats || !getValueFromHistory(todayStats, statType)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherStats = statisticHistory[formatDateMinimal(otherDate)]; // This is now validated
|
||||||
|
|
||||||
|
// Ensure otherStats exists and contains the statType
|
||||||
|
if (!otherStats || !getValueFromHistory(otherStats, statType)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statToday = getValueFromHistory(todayStats, statType);
|
||||||
|
const statOther = getValueFromHistory(otherStats, statType);
|
||||||
|
|
||||||
|
if (statToday === undefined || statOther === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return (statToday - statOther) * (!isNegativeChange ? 1 : -1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatisticChanges = (daysAgo: number): PlayerHistory => {
|
||||||
|
return {
|
||||||
|
rank: getStatisticChange("rank", true, daysAgo),
|
||||||
|
countryRank: getStatisticChange("countryRank", true, daysAgo),
|
||||||
|
pp: getStatisticChange("pp", false, daysAgo),
|
||||||
|
replaysWatched: getStatisticChange("replaysWatched", false, daysAgo),
|
||||||
|
accuracy: {
|
||||||
|
averageRankedAccuracy: getStatisticChange("accuracy.averageRankedAccuracy", false, daysAgo),
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
totalScore: getStatisticChange("score.totalScore", false, daysAgo),
|
||||||
|
totalRankedScore: getStatisticChange("score.totalRankedScore", false, daysAgo),
|
||||||
|
},
|
||||||
|
scores: {
|
||||||
|
totalScores: getStatisticChange("scores.totalScores", false, daysAgo),
|
||||||
|
totalRankedScores: getStatisticChange("scores.totalRankedScores", false, daysAgo),
|
||||||
|
rankedScores: getStatisticChange("scores.rankedScores", false, daysAgo),
|
||||||
|
unrankedScores: getStatisticChange("scores.unrankedScores", false, daysAgo),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: token.id,
|
||||||
|
name: token.name,
|
||||||
|
avatar: token.profilePicture,
|
||||||
|
country: token.country,
|
||||||
|
rank: token.rank,
|
||||||
|
countryRank: token.countryRank,
|
||||||
|
joinedDate: new Date(token.firstSeen),
|
||||||
|
bio: bio,
|
||||||
|
pp: token.pp,
|
||||||
|
statisticChange: {
|
||||||
|
daily: getStatisticChanges(1),
|
||||||
|
weekly: getStatisticChanges(7),
|
||||||
|
monthly: getStatisticChanges(30),
|
||||||
|
},
|
||||||
|
role: token.role == null ? undefined : token.role,
|
||||||
|
badges: badges,
|
||||||
|
statisticHistory: statisticHistory,
|
||||||
|
statistics: token.scoreStats,
|
||||||
|
rankPages: {
|
||||||
|
global: getPageFromRank(token.rank, 50),
|
||||||
|
country: getPageFromRank(token.countryRank, 50),
|
||||||
|
},
|
||||||
|
permissions: token.permissions,
|
||||||
|
banned: token.banned,
|
||||||
|
inactive: token.inactive,
|
||||||
|
isBeingTracked: isBeingTracked,
|
||||||
|
};
|
||||||
|
}
|
@ -6,13 +6,14 @@ import { getAverageColor } from "@/common/image-utils";
|
|||||||
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||||
import { getCookieValue } from "@ssr/common/utils/cookie-utils";
|
import { getCookieValue } from "@ssr/common/utils/cookie-utils";
|
||||||
import { Config } from "@ssr/common/config";
|
import { Config } from "@ssr/common/config";
|
||||||
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player";
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||||
import { ScoreSort } from "@ssr/common/score/score-sort";
|
import { ScoreSort } from "@ssr/common/score/score-sort";
|
||||||
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||||
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
||||||
import { fetchPlayerScores } from "@ssr/common/utils/score-utils";
|
import { fetchPlayerScores } from "@ssr/common/utils/score-utils";
|
||||||
import PlayerScoresResponse from "@ssr/common/response/player-scores-response";
|
import PlayerScoresResponse from "@ssr/common/response/player-scores-response";
|
||||||
import { SSRCache } from "@ssr/common/cache";
|
import { SSRCache } from "@ssr/common/cache";
|
||||||
|
import { getScoreSaberPlayerFromToken } from "@ssr/common/token-creators";
|
||||||
|
|
||||||
const UNKNOWN_PLAYER = {
|
const UNKNOWN_PLAYER = {
|
||||||
title: "ScoreSaber Reloaded - Unknown Player",
|
title: "ScoreSaber Reloaded - Unknown Player",
|
||||||
|
@ -13,11 +13,12 @@ import PlayerCharts from "@/components/player/chart/player-charts";
|
|||||||
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||||
import useDatabase from "@/hooks/use-database";
|
import useDatabase from "@/hooks/use-database";
|
||||||
import { useLiveQuery } from "dexie-react-hooks";
|
import { useLiveQuery } from "dexie-react-hooks";
|
||||||
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player";
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||||
import { ScoreSort } from "@ssr/common/score/score-sort";
|
import { ScoreSort } from "@ssr/common/score/score-sort";
|
||||||
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||||
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
||||||
import PlayerScoresResponse from "@ssr/common/response/player-scores-response";
|
import PlayerScoresResponse from "@ssr/common/response/player-scores-response";
|
||||||
|
import { getScoreSaberPlayerFromToken } from "@ssr/common/token-creators";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialPlayerData: ScoreSaberPlayer;
|
initialPlayerData: ScoreSaberPlayer;
|
||||||
|
@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token";
|
import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token";
|
||||||
import Score from "@/components/score/score";
|
|
||||||
import { parseDate } from "@ssr/common/utils/time-utils";
|
import { parseDate } from "@ssr/common/utils/time-utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||||
import { ScoreSaberWebsocketMessageToken } from "@ssr/common/types/token/scoresaber/websocket/scoresaber-websocket-message";
|
import { ScoreSaberWebsocketMessageToken } from "@ssr/common/types/token/scoresaber/websocket/scoresaber-websocket-message";
|
||||||
import { getScoreSaberScoreFromToken } from "@ssr/common/model/score/impl/scoresaber-score";
|
import Score from "@/components/score/score";
|
||||||
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
import { getScoreSaberLeaderboardFromToken, getScoreSaberScoreFromToken } from "@ssr/common/token-creators";
|
||||||
|
|
||||||
export default function ScoreFeed() {
|
export default function ScoreFeed() {
|
||||||
const { readyState, lastJsonMessage } = useWebSocket<ScoreSaberWebsocketMessageToken>("wss://scoresaber.com/ws");
|
const { readyState, lastJsonMessage } = useWebSocket<ScoreSaberWebsocketMessageToken>("wss://scoresaber.com/ws");
|
||||||
@ -40,12 +39,18 @@ export default function ScoreFeed() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col divide-y divide-border">
|
<div className="flex flex-col divide-y divide-border">
|
||||||
{scores.map(score => {
|
{scores.map(scoreToken => {
|
||||||
const player = score.score.leaderboardPlayerInfo;
|
if (!scoreToken.leaderboard || !scoreToken.score) {
|
||||||
const leaderboard = getScoreSaberLeaderboardFromToken(score.leaderboard);
|
console.error("Invalid leaderboard or score data:", scoreToken);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = scoreToken.score.leaderboardPlayerInfo;
|
||||||
|
const leaderboard = getScoreSaberLeaderboardFromToken(scoreToken.leaderboard);
|
||||||
|
const score = getScoreSaberScoreFromToken(scoreToken.score, leaderboard);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={score.score.id} className="flex flex-col py-2">
|
<div key={score.scoreId} className="flex flex-col py-2">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Set by{" "}
|
Set by{" "}
|
||||||
<Link href={`/player/${player.id}`}>
|
<Link href={`/player/${player.id}`}>
|
||||||
@ -53,7 +58,7 @@ export default function ScoreFeed() {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<Score
|
<Score
|
||||||
score={getScoreSaberScoreFromToken(score.score, leaderboard)}
|
score={score}
|
||||||
leaderboard={leaderboard}
|
leaderboard={leaderboard}
|
||||||
settings={{
|
settings={{
|
||||||
noScoreButtons: true,
|
noScoreButtons: true,
|
||||||
|
@ -23,6 +23,11 @@ import { ScoreStatsToken } from "@ssr/common/types/token/beatleader/score-stats/
|
|||||||
import { beatLeaderService } from "@ssr/common/service/impl/beatleader";
|
import { beatLeaderService } from "@ssr/common/service/impl/beatleader";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
/**
|
||||||
|
* The score to display.
|
||||||
|
*/
|
||||||
|
score: ScoreSaberScore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The leaderboard.
|
* The leaderboard.
|
||||||
*/
|
*/
|
||||||
@ -33,11 +38,6 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
beatSaverMap?: BeatSaverMap;
|
beatSaverMap?: BeatSaverMap;
|
||||||
|
|
||||||
/**
|
|
||||||
* The score to display.
|
|
||||||
*/
|
|
||||||
score: ScoreSaberScore;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Score settings
|
* Score settings
|
||||||
*/
|
*/
|
||||||
@ -123,7 +123,7 @@ export default function Score({ leaderboard, beatSaverMap, score, settings }: Pr
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLeaderboardExpanded(false);
|
setIsLeaderboardExpanded(false);
|
||||||
setLeaderboardDropdownData(undefined);
|
setLeaderboardDropdownData(undefined);
|
||||||
}, [score]);
|
}, [score.scoreId]);
|
||||||
|
|
||||||
const accuracy = (baseScore / leaderboard.maxScore) * 100;
|
const accuracy = (baseScore / leaderboard.maxScore) * 100;
|
||||||
const pp = baseScore === score.score ? score.pp : scoresaberService.getPp(leaderboard.stars, accuracy);
|
const pp = baseScore === score.score ? score.pp : scoresaberService.getPp(leaderboard.stars, accuracy);
|
||||||
|
Reference in New Issue
Block a user