From f3dee6a7d2da1fde1d03d4a523bb5033ee342b0d Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 22 Oct 2024 17:30:14 +0100 Subject: [PATCH] rework beatleader data tracking --- projects/backend/src/service/score.service.ts | 62 +++++++++++-- .../impl/scoresaber-leaderboard.ts | 2 +- .../additional-score-data.ts | 73 ++++++++++------ .../additional-score-data/hand-accuracy.ts | 15 ++++ .../src/model/additional-score-data/misses.ts | 33 +++++++ projects/common/src/score/score.ts | 2 +- projects/website/src/common/change.tsx | 21 +++++ .../components/score/badges/score-misses.tsx | 86 +++++++++++++------ .../src/components/score/score-buttons.tsx | 2 +- .../components/score/score-misses-tooltip.tsx | 50 +++++++++++ .../src/components/score/score-stats.tsx | 40 +++++++-- .../website/src/components/stat-value.tsx | 2 +- 12 files changed, 317 insertions(+), 71 deletions(-) rename projects/common/src/model/{ => additional-score-data}/additional-score-data.ts (64%) create mode 100644 projects/common/src/model/additional-score-data/hand-accuracy.ts create mode 100644 projects/common/src/model/additional-score-data/misses.ts create mode 100644 projects/website/src/common/change.tsx create mode 100644 projects/website/src/components/score/score-misses-tooltip.tsx diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts index 8717f26..2b25f23 100644 --- a/projects/backend/src/service/score.service.ts +++ b/projects/backend/src/service/score.service.ts @@ -25,7 +25,10 @@ import { SSRCache } from "@ssr/common/cache"; import { fetchWithCache } from "../common/cache.util"; import { PlayerDocument, PlayerModel } from "@ssr/common/model/player"; import { BeatLeaderScoreToken } from "@ssr/common/types/token/beatleader/beatleader-score-token"; -import { AdditionalScoreData, AdditionalScoreDataModel } from "@ssr/common/model/additional-score-data"; +import { + AdditionalScoreData, + AdditionalScoreDataModel, +} from "../../../common/src/model/additional-score-data/additional-score-data"; const playerScoresCache = new SSRCache({ ttl: 1000 * 60, // 1 minute @@ -167,22 +170,65 @@ export class ScoreService { return; } + // The score has already been tracked, so ignore it. + if ( + (await this.getAdditionalScoreData( + playerId, + leaderboard.song.hash, + leaderboard.difficulty.difficultyName, + score.baseScore + )) !== undefined + ) { + return; + } + const difficulty = leaderboard.difficulty; const difficultyKey = `${difficulty.difficultyName.replace("Plus", "+")}-${difficulty.modeName}`; - await AdditionalScoreDataModel.create({ + const rawScoreImprovement = score.scoreImprovement; + const data = { playerId: playerId, - songHash: leaderboard.song.hash, + songHash: leaderboard.song.hash.toUpperCase(), songDifficulty: difficultyKey, songScore: score.baseScore, - bombCuts: score.bombCuts, - wallsHit: score.wallsHit, + misses: { + misses: score.missedNotes + score.badCuts, + missedNotes: score.missedNotes, + bombCuts: score.bombCuts, + badCuts: score.badCuts, + wallsHit: score.wallsHit, + }, pauses: score.pauses, fcAccuracy: score.fcAccuracy * 100, + fullCombo: score.fullCombo, handAccuracy: { left: score.accLeft, right: score.accRight, }, - } as AdditionalScoreData); + } as AdditionalScoreData; + if (rawScoreImprovement.score > 0) { + data.scoreImprovement = { + score: rawScoreImprovement.score, + misses: { + misses: rawScoreImprovement.missedNotes + rawScoreImprovement.badCuts, + missedNotes: rawScoreImprovement.missedNotes, + bombCuts: rawScoreImprovement.bombCuts, + badCuts: rawScoreImprovement.badCuts, + wallsHit: rawScoreImprovement.wallsHit, + }, + accuracy: rawScoreImprovement.accuracy * 100, + fullCombo: + rawScoreImprovement.missedNotes == 0 && + rawScoreImprovement.bombCuts == 0 && + rawScoreImprovement.badCuts == 0 && + rawScoreImprovement.wallsHit == 0, + handAccuracy: { + left: rawScoreImprovement.accLeft, + right: rawScoreImprovement.accRight, + }, + }; + } + + await AdditionalScoreDataModel.create(data); console.log( `Tracked additional score data for "${scorePlayer.name}"(${playerId}), difficulty: ${difficultyKey}, score: ${score.baseScore}` ); @@ -205,7 +251,7 @@ export class ScoreService { ): Promise { const additionalData = await AdditionalScoreDataModel.findOne({ playerId: playerId, - songHash: songHash, + songHash: songHash.toUpperCase(), songDifficulty: songDifficulty, songScore: songScore, }); @@ -232,7 +278,6 @@ export class ScoreService { sort: string, search?: string ): Promise | undefined> { - console.log("hi"); return fetchWithCache( playerScoresCache, `player-scores-${leaderboardName}-${id}-${page}-${sort}-${search}`, @@ -275,7 +320,6 @@ export class ScoreService { `${tokenLeaderboard.difficulty.difficulty}-${tokenLeaderboard.difficulty.gameMode}`, score.score ); - console.log("additionalData", additionalData); if (additionalData !== undefined) { score.additionalData = additionalData; } diff --git a/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts b/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts index df8ef39..9ca59ed 100644 --- a/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts +++ b/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts @@ -54,7 +54,7 @@ export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardTo return { id: token.id, - songHash: token.songHash, + songHash: token.songHash.toUpperCase(), songName: token.songName, songSubName: token.songSubName, songAuthorName: token.songAuthorName, diff --git a/projects/common/src/model/additional-score-data.ts b/projects/common/src/model/additional-score-data/additional-score-data.ts similarity index 64% rename from projects/common/src/model/additional-score-data.ts rename to projects/common/src/model/additional-score-data/additional-score-data.ts index 1db676d..89b1f0e 100644 --- a/projects/common/src/model/additional-score-data.ts +++ b/projects/common/src/model/additional-score-data/additional-score-data.ts @@ -1,5 +1,7 @@ import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose"; import { Document } from "mongoose"; +import { HandAccuracy } from "./hand-accuracy"; +import { Misses } from "./misses"; /** * The model for a BeatSaver map. @@ -47,18 +49,8 @@ export class AdditionalScoreData { @prop({ required: true, index: true }) public songScore!: number; - /** - * The amount of times a bomb was hit. - */ - - @prop({ required: false }) - public bombCuts!: number; - - /** - * The amount of walls hit in the play. - */ - @prop({ required: false }) - public wallsHit!: number; + // Above data is only so we can fetch it + // -------------------------------- /** * The amount of pauses in the play. @@ -66,28 +58,61 @@ export class AdditionalScoreData { @prop({ required: false }) public pauses!: number; + /** + * The miss data for the play. + */ + @prop({ required: false, _id: false }) + public misses!: Misses; + /** * The hand accuracy for each hand. * @private */ - @prop({ required: false }) - public handAccuracy!: { - /** - * The left hand accuracy. - */ - left: number; - - /** - * The right hand accuracy. - */ - right: number; - }; + @prop({ required: false, _id: false }) + public handAccuracy!: HandAccuracy; /** * The full combo accuracy of the play. */ @prop({ required: true }) public fcAccuracy!: number; + + /** + * Whether the play was a full combo. + */ + @prop({ required: true }) + public fullCombo!: boolean; + + /** + * The score improvement. + */ + @prop({ required: false, _id: false }) + public scoreImprovement?: { + /** + * The change in the score. + */ + score: number; + + /** + * The change in the accuracy. + */ + accuracy: number; + + /** + * The change in the misses. + */ + misses: Misses; + + /** + * Whether the play was a full combo. + */ + fullCombo: boolean; + + /** + * The change in the hand accuracy. + */ + handAccuracy: HandAccuracy; + }; } export type AdditionalScoreDataDocument = AdditionalScoreData & Document; diff --git a/projects/common/src/model/additional-score-data/hand-accuracy.ts b/projects/common/src/model/additional-score-data/hand-accuracy.ts new file mode 100644 index 0000000..d37d261 --- /dev/null +++ b/projects/common/src/model/additional-score-data/hand-accuracy.ts @@ -0,0 +1,15 @@ +import { prop } from "@typegoose/typegoose"; + +export class HandAccuracy { + /** + * The left hand accuracy. + */ + @prop({ required: true }) + left!: number; + + /** + * The right hand accuracy. + */ + @prop({ required: true }) + right!: number; +} diff --git a/projects/common/src/model/additional-score-data/misses.ts b/projects/common/src/model/additional-score-data/misses.ts new file mode 100644 index 0000000..d82c06d --- /dev/null +++ b/projects/common/src/model/additional-score-data/misses.ts @@ -0,0 +1,33 @@ +import { prop } from "@typegoose/typegoose"; + +export class Misses { + /** + * The amount of misses notes + bad cuts. + */ + @prop({ required: true }) + misses!: number; + + /** + * The total amount of notes that were missed. + */ + @prop({ required: true }) + missedNotes!: number; + + /** + * The amount of times a bomb was hit. + */ + @prop({ required: true }) + bombCuts!: number; + + /** + * The amount of walls hit in the play. + */ + @prop({ required: true }) + wallsHit!: number; + + /** + * The number of bad cuts. + */ + @prop({ required: true }) + badCuts!: number; +} diff --git a/projects/common/src/score/score.ts b/projects/common/src/score/score.ts index 2426d8e..fc78648 100644 --- a/projects/common/src/score/score.ts +++ b/projects/common/src/score/score.ts @@ -1,6 +1,6 @@ import { Modifier } from "./modifier"; import { Leaderboards } from "../leaderboard"; -import { AdditionalScoreData } from "../model/additional-score-data"; +import { AdditionalScoreData } from "../model/additional-score-data/additional-score-data"; export default interface Score { /** diff --git a/projects/website/src/common/change.tsx b/projects/website/src/common/change.tsx new file mode 100644 index 0000000..db98c4e --- /dev/null +++ b/projects/website/src/common/change.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +/** + * Renders the change for a stat. + * + * @param change the change + * @param isPp whether the stat is pp + * @param formatValue the function to format the value + */ +export function renderChange(change: number | undefined, isPp: boolean, formatValue: (value: number) => string) { + if (change === 0 || (change && change < 0.01) || change === undefined) { + return null; + } + + return ( +

0 ? "text-green-400" : "text-red-400"}`}> + {change > 0 ? "+" : ""} + {`${formatValue(change)}${isPp ? "pp" : ""}`} +

+ ); +} diff --git a/projects/website/src/components/score/badges/score-misses.tsx b/projects/website/src/components/score/badges/score-misses.tsx index 380d953..adbad3a 100644 --- a/projects/website/src/components/score/badges/score-misses.tsx +++ b/projects/website/src/components/score/badges/score-misses.tsx @@ -1,8 +1,9 @@ import { formatNumberWithCommas } from "@ssr/common/utils/number-utils"; import { XMarkIcon } from "@heroicons/react/24/solid"; import clsx from "clsx"; -import Tooltip from "@/components/tooltip"; import { ScoreBadgeProps } from "@/components/score/badges/badge-props"; +import { ScoreMissesTooltip } from "@/components/score/score-misses-tooltip"; +import { Misses } from "@ssr/common/model/additional-score-data/misses"; type ScoreMissesBadgeProps = ScoreBadgeProps & { /** @@ -12,32 +13,65 @@ type ScoreMissesBadgeProps = ScoreBadgeProps & { }; export default function ScoreMissesBadge({ score, hideXMark }: ScoreMissesBadgeProps) { + const additionalData = score.additionalData; + const scoreImprovement = additionalData?.scoreImprovement; + + const misses = additionalData?.misses; + const previousScoreMisses: Misses | undefined = misses && + additionalData && + scoreImprovement && { + misses: misses.misses - scoreImprovement.misses.misses, + missedNotes: misses.missedNotes - scoreImprovement.misses.missedNotes, + badCuts: misses.badCuts - scoreImprovement.misses.badCuts, + bombCuts: misses.bombCuts - scoreImprovement.misses.bombCuts, + wallsHit: misses.wallsHit - scoreImprovement.misses.wallsHit, + }; + return ( - - {!score.fullCombo ? ( - <> -

Misses

-

Missed Notes: {formatNumberWithCommas(score.missedNotes)}

-

Bad Cuts: {formatNumberWithCommas(score.badCuts)}

- {score.additionalData && ( - <> -

Bomb Cuts: {formatNumberWithCommas(score.additionalData.bombCuts)}

-

Wall Hits: {formatNumberWithCommas(score.additionalData.wallsHit)}

- - )} - - ) : ( -

Full Combo

- )} +
+ +
+

{score.fullCombo ? FC : formatNumberWithCommas(score.misses)}

+ {!hideXMark && }
- } - > -
-

{score.fullCombo ? FC : formatNumberWithCommas(score.misses)}

- {!hideXMark && } -
- +
+ {additionalData && previousScoreMisses && scoreImprovement && misses && ( +
+ +
+ {previousScoreMisses.missedNotes == 0 ? ( +

FC

+ ) : ( + formatNumberWithCommas(previousScoreMisses.misses) + )} +
+
+

->

+ +
+ {additionalData.fullCombo ?

FC

: formatNumberWithCommas(misses.misses)} +
+
+
+ )} +
); } diff --git a/projects/website/src/components/score/score-buttons.tsx b/projects/website/src/components/score/score-buttons.tsx index 2124531..fed16f6 100644 --- a/projects/website/src/components/score/score-buttons.tsx +++ b/projects/website/src/components/score/score-buttons.tsx @@ -35,7 +35,7 @@ export default function ScoreButtons({ const { toast } = useToast(); return ( -
+
diff --git a/projects/website/src/components/score/score-misses-tooltip.tsx b/projects/website/src/components/score/score-misses-tooltip.tsx new file mode 100644 index 0000000..1862160 --- /dev/null +++ b/projects/website/src/components/score/score-misses-tooltip.tsx @@ -0,0 +1,50 @@ +import { formatNumberWithCommas } from "@ssr/common/utils/number-utils"; +import Tooltip from "@/components/tooltip"; + +type ScoreMissesTooltipProps = { + missedNotes: number; + badCuts: number; + bombCuts?: number; + wallsHit?: number; + fullCombo?: boolean; + + /** + * The tooltip children + */ + children: React.ReactNode; +}; + +export function ScoreMissesTooltip({ + missedNotes, + badCuts, + bombCuts, + wallsHit, + fullCombo, + children, +}: ScoreMissesTooltipProps) { + return ( + + {!fullCombo ? ( + <> +

Misses

+

Missed Notes: {formatNumberWithCommas(missedNotes)}

+

Bad Cuts: {formatNumberWithCommas(badCuts)}

+ {bombCuts !== undefined && wallsHit !== undefined && ( + <> +

Bomb Cuts: {formatNumberWithCommas(bombCuts)}

+

Wall Hits: {formatNumberWithCommas(wallsHit)}

+ + )} + + ) : ( +

Full Combo

+ )} +
+ } + > + {children} + + ); +} diff --git a/projects/website/src/components/score/score-stats.tsx b/projects/website/src/components/score/score-stats.tsx index ddc3a8c..9cd7621 100644 --- a/projects/website/src/components/score/score-stats.tsx +++ b/projects/website/src/components/score/score-stats.tsx @@ -7,6 +7,7 @@ import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leade import ScoreMissesBadge from "@/components/score/badges/score-misses"; import { Modifier } from "@ssr/common/score/modifier"; import { ScoreModifiers } from "@/components/score/score-modifiers"; +import { renderChange } from "@/common/change"; const badges: ScoreBadge[] = [ { @@ -48,6 +49,8 @@ const badges: ScoreBadge[] = [ return getScoreBadgeFromAccuracy(acc).color; }, create: (score: ScoreSaberScore, leaderboard: ScoreSaberLeaderboard) => { + const scoreImprovement = score.additionalData?.scoreImprovement; + const acc = (score.score / leaderboard.maxScore) * 100; const fcAccuracy = score.additionalData?.fcAccuracy; const scoreBadge = getScoreBadgeFromAccuracy(acc); @@ -83,9 +86,15 @@ const badges: ScoreBadge[] = [
} > -

- {acc.toFixed(2)}% {modCount > 0 && } -

+
+

+ {acc.toFixed(2)}% {modCount > 0 && } +

+ {scoreImprovement && + renderChange(scoreImprovement.accuracy, false, num => { + return `${num.toFixed(2)}%`; + })} +
); @@ -94,7 +103,14 @@ const badges: ScoreBadge[] = [ { name: "Score", create: (score: ScoreSaberScore) => { - return `${formatNumberWithCommas(Number(score.score.toFixed(0)))}`; + const scoreImprovement = score.additionalData?.scoreImprovement; + + return ( +
+

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

+ {scoreImprovement && renderChange(scoreImprovement.score, false, formatNumberWithCommas)} +
+ ); }, }, { @@ -104,11 +120,15 @@ const badges: ScoreBadge[] = [ if (!score.additionalData) { return undefined; } - const { handAccuracy } = score.additionalData; + const scoreImprovement = score.additionalData.scoreImprovement; + return ( -

{handAccuracy.left.toFixed(2)}

+
+

{handAccuracy.left.toFixed(2)}

+ {scoreImprovement && renderChange(scoreImprovement.handAccuracy.left, false, num => num.toFixed(2))} +
); }, @@ -122,9 +142,13 @@ const badges: ScoreBadge[] = [ } const { handAccuracy } = score.additionalData; + const scoreImprovement = score.additionalData.scoreImprovement; return ( -

{handAccuracy.right.toFixed(2)}

+
+

{handAccuracy.right.toFixed(2)}

+ {scoreImprovement && renderChange(scoreImprovement.handAccuracy.right, false, num => num.toFixed(2))} +
); }, @@ -144,7 +168,7 @@ type Props = { export default function ScoreStats({ score, leaderboard }: Props) { return ( -
+
); diff --git a/projects/website/src/components/stat-value.tsx b/projects/website/src/components/stat-value.tsx index c73bdd3..b632b18 100644 --- a/projects/website/src/components/stat-value.tsx +++ b/projects/website/src/components/stat-value.tsx @@ -21,7 +21,7 @@ export default function StatValue({ name, color, value }: Props) { return (