diff --git a/projects/backend/src/controller/scores.controller.ts b/projects/backend/src/controller/scores.controller.ts index 8c59e7e..4458f19 100644 --- a/projects/backend/src/controller/scores.controller.ts +++ b/projects/backend/src/controller/scores.controller.ts @@ -71,6 +71,6 @@ export default class ScoresController { }; query: { search?: string }; }): Promise { - return (await ScoreService.getPreviousScores(playerId, leaderboardId, page)).toJSON(); + return (await ScoreService.getScoreHistory(playerId, leaderboardId, page)).toJSON(); } } diff --git a/projects/backend/src/index.ts b/projects/backend/src/index.ts index 6e53aeb..6aa92f0 100644 --- a/projects/backend/src/index.ts +++ b/projects/backend/src/index.ts @@ -61,27 +61,30 @@ connectBeatLeaderWebsocket({ }); export const app = new Elysia(); -app.use( - cron({ - name: "player-statistics-tracker-cron", - pattern: "1 0 * * *", // Every day at 00:01 - timezone: "Europe/London", // UTC time - run: async () => { - await PlayerService.updatePlayerStatistics(); - }, - }) -); -app.use( - cron({ - name: "player-scores-tracker-cron", - pattern: "0 4 * * *", // Every day at 04:00 - timezone: "Europe/London", // UTC time - protect: true, - run: async () => { - await PlayerService.refreshPlayerScores(); - }, - }) -); +if (isProduction()) { + app.use( + cron({ + name: "player-statistics-tracker-cron", + pattern: "1 0 * * *", // Every day at 00:01 + timezone: "Europe/London", // UTC time + protect: true, + run: async () => { + await PlayerService.updatePlayerStatistics(); + }, + }) + ); + app.use( + cron({ + name: "player-scores-tracker-cron", + pattern: "0 4 * * *", // Every day at 04:00 + timezone: "Europe/London", // UTC time + protect: true, + run: async () => { + await PlayerService.refreshPlayerScores(); + }, + }) + ); +} /** * Custom error handler diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts index 5377496..166527d 100644 --- a/projects/backend/src/service/score.service.ts +++ b/projects/backend/src/service/score.service.ts @@ -7,7 +7,6 @@ import BeatSaverService from "./beatsaver.service"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { ScoreSort } from "@ssr/common/score/score-sort"; import { Leaderboards } from "@ssr/common/leaderboard"; -import Leaderboard from "@ssr/common/leaderboard/leaderboard"; import LeaderboardService from "./leaderboard.service"; import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; import { PlayerScore } from "@ssr/common/score/player-score"; @@ -27,13 +26,18 @@ import { import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement"; import { ScoreType } from "@ssr/common/model/score/score"; import { getScoreSaberLeaderboardFromToken, getScoreSaberScoreFromToken } from "@ssr/common/token-creators"; -import { ScoreSaberScore, ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score"; -import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; +import { + ScoreSaberPreviousScore, + ScoreSaberScore, + ScoreSaberScoreModel, +} from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; import { MapDifficulty } from "@ssr/common/score/map-difficulty"; import { MapCharacteristic } from "@ssr/common/types/map-characteristic"; import { Page, Pagination } from "@ssr/common/pagination"; +import ScoreSaberLeaderboard from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard"; +import Leaderboard from "@ssr/common/model/leaderboard/leaderboard"; const playerScoresCache = new SSRCache({ ttl: 1000 * 60, // 1 minute @@ -394,6 +398,10 @@ export class ScoreService { if (additionalData !== undefined) { score.additionalData = additionalData; } + const previousScore = await this.getPreviousScore(playerId, leaderboard.id + "", score.timestamp); + if (previousScore !== undefined) { + score.previousScore = previousScore; + } scores.push({ score: score, @@ -491,13 +499,13 @@ export class ScoreService { } /** - * Gets the previous scores for a player. + * Gets the player's score history for a map. * * @param playerId the player's id to get the previous scores for * @param leaderboardId the leaderboard to get the previous scores on * @param page the page to get */ - public static async getPreviousScores( + public static async getScoreHistory( playerId: string, leaderboardId: string, page: number @@ -533,6 +541,10 @@ export class ScoreService { if (additionalData !== undefined) { score.additionalData = additionalData; } + const previousScore = await this.getPreviousScore(playerId, leaderboardId, score.timestamp); + if (previousScore !== undefined) { + score.previousScore = previousScore; + } toReturn.push({ score: score as unknown as ScoreSaberScore, @@ -544,4 +556,55 @@ export class ScoreService { return toReturn; }); } + + /** + * Gets the player's previous score for a map. + * + * @param playerId the player's id to get the previous score for + * @param leaderboardId the leaderboard to get the previous score on + * @param timestamp the score's timestamp to get the previous score for + * @returns the score, or undefined if none + */ + public static async getPreviousScore( + playerId: string, + leaderboardId: string, + timestamp: Date + ): Promise { + const scores = await ScoreSaberScoreModel.find({ playerId: playerId, leaderboardId: leaderboardId }); + if (scores == null || scores.length == 0) { + return undefined; + } + + const scoreIndex = scores.findIndex(score => score.timestamp.getTime() == timestamp.getTime()); + const score = scores.find(score => score.timestamp.getTime() == timestamp.getTime()); + if (scoreIndex == -1 || score == undefined) { + return undefined; + } + const previousScore = scores[scoreIndex - 1]; + if (previousScore == undefined) { + return undefined; + } + return { + score: previousScore.score, + accuracy: previousScore.accuracy, + modifiers: previousScore.modifiers, + misses: previousScore.misses, + missedNotes: previousScore.missedNotes, + badCuts: previousScore.badCuts, + fullCombo: previousScore.fullCombo, + pp: previousScore.pp, + weight: previousScore.weight, + maxCombo: previousScore.maxCombo, + change: { + score: score.score - previousScore.score, + accuracy: score.accuracy - previousScore.accuracy, + misses: score.misses - previousScore.misses, + missedNotes: score.missedNotes - previousScore.missedNotes, + badCuts: score.badCuts - previousScore.badCuts, + pp: score.pp - previousScore.pp, + weight: score.weight && previousScore.weight && score.weight - previousScore.weight, + maxCombo: score.maxCombo - previousScore.maxCombo, + }, + } as ScoreSaberPreviousScore; + } } diff --git a/projects/common/src/model/score/impl/scoresaber-score.ts b/projects/common/src/model/score/impl/scoresaber-score.ts index 9b3418a..4e08a0c 100644 --- a/projects/common/src/model/score/impl/scoresaber-score.ts +++ b/projects/common/src/model/score/impl/scoresaber-score.ts @@ -3,6 +3,7 @@ import Score from "../score"; import { type ScoreSaberLeaderboardPlayerInfoToken } from "../../../types/token/scoresaber/score-saber-leaderboard-player-info-token"; import { Document } from "mongoose"; import { AutoIncrementID } from "@typegoose/auto-increment"; +import { PreviousScore } from "../previous-score"; @modelOptions({ options: { allowMixed: Severity.ALLOW }, @@ -58,6 +59,11 @@ export class ScoreSaberScoreInternal extends Score { */ @Prop({ required: true }) public readonly maxCombo!: number; + + /** + * The previous score, if any. + */ + public previousScore?: ScoreSaberPreviousScore; } class ScoreSaberScorePublic extends ScoreSaberScoreInternal { @@ -67,6 +73,28 @@ class ScoreSaberScorePublic extends ScoreSaberScoreInternal { public playerInfo!: ScoreSaberLeaderboardPlayerInfoToken; } +export type ScoreSaberPreviousScore = PreviousScore & { + /** + * The pp of the previous score. + */ + pp: number; + + /** + * The weight of the previous score. + */ + weight: number; + + /** + * The max combo of the previous score. + */ + maxCombo: number; + + /** + * The change between the previous score and the current score. + */ + change?: ScoreSaberPreviousScore; +}; + export type ScoreSaberScore = InstanceType; export type ScoreSaberScoreDocument = ScoreSaberScore & Document; export const ScoreSaberScoreModel: ReturnModelType = diff --git a/projects/common/src/model/score/previous-score.ts b/projects/common/src/model/score/previous-score.ts new file mode 100644 index 0000000..491ed45 --- /dev/null +++ b/projects/common/src/model/score/previous-score.ts @@ -0,0 +1,38 @@ +import { Modifier } from "../../score/modifier"; + +export type PreviousScore = { + /** + * The score of the previous score. + */ + score: number; + + /** + * The accuracy of the previous score. + */ + accuracy: number; + + /** + * The modifiers of the previous score. + */ + modifiers?: Modifier[]; + + /** + * The misses of the previous score. + */ + misses: number; + + /** + * The missed notes of the previous score. + */ + missedNotes: number; + + /** + * The bad cuts of the previous score. + */ + badCuts: number; + + /** + * The full combo of the previous score. + */ + fullCombo?: boolean; +}; diff --git a/projects/website/src/components/score/badges/score-accuracy.tsx b/projects/website/src/components/score/badges/score-accuracy.tsx index 48c9ed7..c975304 100644 --- a/projects/website/src/components/score/badges/score-accuracy.tsx +++ b/projects/website/src/components/score/badges/score-accuracy.tsx @@ -14,8 +14,7 @@ type ScoreAccuracyProps = ScoreBadgeProps & { }; export function ScoreAccuracyBadge({ score, leaderboard }: ScoreAccuracyProps) { - const scoreImprovement = score.additionalData?.scoreImprovement; - const previousAccuracy = scoreImprovement ? score.accuracy - scoreImprovement.accuracy : undefined; + const previousScore = score.previousScore; const fcAccuracy = score.additionalData?.fcAccuracy; const scoreBadge = getScoreBadgeFromAccuracy(score.accuracy); @@ -57,9 +56,13 @@ export function ScoreAccuracyBadge({ score, leaderboard }: ScoreAccuracyProps) { {modCount > 0 && }

- {scoreImprovement && previousAccuracy && ( - - `${num.toFixed(2)}%`} /> + {previousScore && previousScore.change && ( + + `${num.toFixed(2)}%`} + /> )} diff --git a/projects/website/src/components/score/badges/score-pp.tsx b/projects/website/src/components/score/badges/score-pp.tsx index 674414b..7f02076 100644 --- a/projects/website/src/components/score/badges/score-pp.tsx +++ b/projects/website/src/components/score/badges/score-pp.tsx @@ -4,7 +4,6 @@ import Tooltip from "@/components/tooltip"; import { ensurePositiveNumber, formatPp } from "@ssr/common/utils/number-utils"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { Change } from "@/common/change"; -import { Warning } from "@/components/warning"; type ScorePpProps = ScoreBadgeProps & { /** @@ -14,8 +13,7 @@ type ScorePpProps = ScoreBadgeProps & { }; export function ScorePpBadge({ score, leaderboard }: ScorePpProps) { - const scoreImprovement = score.additionalData?.scoreImprovement; - const previousAccuracy = scoreImprovement ? score.accuracy - scoreImprovement?.accuracy : undefined; + const previousScore = score.previousScore; const fcAccuracy = score.additionalData?.fcAccuracy; const pp = score.pp; const weight = score.weight; @@ -28,39 +26,29 @@ export function ScorePpBadge({ score, leaderboard }: ScorePpProps) { return ( <> - -
-

Performance Points

-

Raw: {formatPp(pp)}pp

-

- Weighted: {formatPp(weightedPp)}pp ({(100 * weight).toFixed(2)}%) -

- {fcPp &&

Full Combo: {fcPp}pp

} -
- - {previousAccuracy && ( - -

- The previous pp may not be 100% accurate due to ScoreSaber API limitations. +

+ +
+

Performance Points

+

Raw: {formatPp(pp)}pp

+

+ Weighted: {formatPp(weightedPp)}pp ({(100 * weight).toFixed(2)}%)

- - )} -
- } - > -
+ {fcPp &&

Full Combo: {fcPp}pp

} +
+
+ } + >

{formatPp(pp)}pp

- {previousAccuracy && ( - - )} - -
+
+ {previousScore && previousScore.change && ( + Previous PP: {formatPp(previousScore.pp)}pp

}> + +
+ )} + ); } diff --git a/projects/website/src/components/score/badges/score-score.tsx b/projects/website/src/components/score/badges/score-score.tsx index 977f73e..488036a 100644 --- a/projects/website/src/components/score/badges/score-score.tsx +++ b/projects/website/src/components/score/badges/score-score.tsx @@ -1,14 +1,19 @@ import { ScoreBadgeProps } from "@/components/score/badges/badge-props"; import { formatNumberWithCommas } from "@ssr/common/utils/number-utils"; import { Change } from "@/common/change"; +import Tooltip from "@/components/tooltip"; export function ScoreScoreBadge({ score }: ScoreBadgeProps) { - const scoreImprovement = score.additionalData?.scoreImprovement; + const previousScore = score.previousScore; return (

{formatNumberWithCommas(Number(score.score.toFixed(0)))}

- {scoreImprovement && } + {previousScore && previousScore.change && ( + Previous Score: {previousScore.score}

}> + +
+ )}
); }