fix crying
This commit is contained in:
@ -32,54 +32,3 @@ export default interface ScoreSaberLeaderboard extends Leaderboard {
|
||||
*/
|
||||
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>;
|
||||
|
||||
/**
|
||||
* 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 const ScoreSaberScoreModel: ReturnModelType<typeof ScoreSaberScoreInternal> =
|
||||
getModelForClass(ScoreSaberScoreInternal);
|
||||
|
@ -73,223 +73,6 @@ export default interface ScoreSaberPlayer extends Player {
|
||||
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.
|
||||
*/
|
||||
|
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,
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user