rework beatleader data tracking
This commit is contained in:
parent
fa2ba83c7a
commit
f3dee6a7d2
@ -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,
|
||||
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<AdditionalScoreData | undefined> {
|
||||
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<PlayerScoresResponse<unknown, unknown> | 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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
33
projects/common/src/model/additional-score-data/misses.ts
Normal file
33
projects/common/src/model/additional-score-data/misses.ts
Normal file
@ -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;
|
||||
}
|
@ -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 {
|
||||
/**
|
||||
|
21
projects/website/src/common/change.tsx
Normal file
21
projects/website/src/common/change.tsx
Normal file
@ -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 (
|
||||
<p className={`text-sm ${change > 0 ? "text-green-400" : "text-red-400"}`}>
|
||||
{change > 0 ? "+" : ""}
|
||||
{`${formatValue(change)}${isPp ? "pp" : ""}`}
|
||||
</p>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<Tooltip
|
||||
display={
|
||||
<div className="flex flex-col">
|
||||
{!score.fullCombo ? (
|
||||
<>
|
||||
<p className="font-semibold">Misses</p>
|
||||
<p>Missed Notes: {formatNumberWithCommas(score.missedNotes)}</p>
|
||||
<p>Bad Cuts: {formatNumberWithCommas(score.badCuts)}</p>
|
||||
{score.additionalData && (
|
||||
<>
|
||||
<p>Bomb Cuts: {formatNumberWithCommas(score.additionalData.bombCuts)}</p>
|
||||
<p>Wall Hits: {formatNumberWithCommas(score.additionalData.wallsHit)}</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>Full Combo</p>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<ScoreMissesTooltip
|
||||
missedNotes={score.missedNotes}
|
||||
badCuts={score.badCuts}
|
||||
bombCuts={misses?.bombCuts}
|
||||
wallsHit={misses?.wallsHit}
|
||||
fullCombo={score.fullCombo}
|
||||
>
|
||||
<div className="flex gap-1 items-center justify-center">
|
||||
<div className="flex gap-1 items-center">
|
||||
<p>{score.fullCombo ? <span className="text-green-400">FC</span> : formatNumberWithCommas(score.misses)}</p>
|
||||
{!hideXMark && <XMarkIcon className={clsx("w-5 h-5", score.fullCombo ? "hidden" : "text-red-400")} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</ScoreMissesTooltip>
|
||||
{additionalData && previousScoreMisses && scoreImprovement && misses && (
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<ScoreMissesTooltip
|
||||
missedNotes={previousScoreMisses.missedNotes}
|
||||
badCuts={previousScoreMisses.badCuts}
|
||||
bombCuts={previousScoreMisses.bombCuts}
|
||||
wallsHit={previousScoreMisses.wallsHit}
|
||||
fullCombo={scoreImprovement.fullCombo}
|
||||
>
|
||||
<div className="flex gap-1 items-center">
|
||||
{previousScoreMisses.missedNotes == 0 ? (
|
||||
<p className="text-green-400">FC</p>
|
||||
) : (
|
||||
formatNumberWithCommas(previousScoreMisses.misses)
|
||||
)}
|
||||
</div>
|
||||
</ScoreMissesTooltip>
|
||||
<p>-></p>
|
||||
<ScoreMissesTooltip
|
||||
missedNotes={misses.missedNotes}
|
||||
badCuts={misses.badCuts}
|
||||
bombCuts={misses.bombCuts}
|
||||
wallsHit={misses.wallsHit}
|
||||
fullCombo={additionalData.fullCombo}
|
||||
>
|
||||
<div className="flex gap-1 items-center">
|
||||
{additionalData.fullCombo ? <p className="text-green-400">FC</p> : formatNumberWithCommas(misses.misses)}
|
||||
</div>
|
||||
</ScoreMissesTooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ export default function ScoreButtons({
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<div className={`flex justify-end gap-2 h-[${alwaysSingleLine ? "32" : "64"}px]`}>
|
||||
<div className={`flex justify-end gap-2 items-center`}>
|
||||
<div
|
||||
className={`flex ${alwaysSingleLine ? "flex-nowrap" : "flex-wrap"} items-center lg:items-start justify-center lg:justify-end gap-1`}
|
||||
>
|
||||
|
@ -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 (
|
||||
<Tooltip
|
||||
display={
|
||||
<div className="flex flex-col">
|
||||
{!fullCombo ? (
|
||||
<>
|
||||
<p className="font-semibold">Misses</p>
|
||||
<p>Missed Notes: {formatNumberWithCommas(missedNotes)}</p>
|
||||
<p>Bad Cuts: {formatNumberWithCommas(badCuts)}</p>
|
||||
{bombCuts !== undefined && wallsHit !== undefined && (
|
||||
<>
|
||||
<p>Bomb Cuts: {formatNumberWithCommas(bombCuts)}</p>
|
||||
<p>Wall Hits: {formatNumberWithCommas(wallsHit)}</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>Full Combo</p>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
@ -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[] = [
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="cursor-default">
|
||||
<div className="flex flex-col items-center justify-center cursor-default">
|
||||
<p>
|
||||
{acc.toFixed(2)}% {modCount > 0 && <ScoreModifiers type="simple" limit={1} score={score} />}
|
||||
</p>
|
||||
{scoreImprovement &&
|
||||
renderChange(scoreImprovement.accuracy, false, num => {
|
||||
return `${num.toFixed(2)}%`;
|
||||
})}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
@ -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 (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p>{formatNumberWithCommas(Number(score.score.toFixed(0)))}</p>
|
||||
{scoreImprovement && renderChange(scoreImprovement.score, false, formatNumberWithCommas)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -104,11 +120,15 @@ const badges: ScoreBadge[] = [
|
||||
if (!score.additionalData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { handAccuracy } = score.additionalData;
|
||||
const scoreImprovement = score.additionalData.scoreImprovement;
|
||||
|
||||
return (
|
||||
<Tooltip display={"Left Hand Accuracy"}>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p>{handAccuracy.left.toFixed(2)}</p>
|
||||
{scoreImprovement && renderChange(scoreImprovement.handAccuracy.left, false, num => num.toFixed(2))}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
@ -122,9 +142,13 @@ const badges: ScoreBadge[] = [
|
||||
}
|
||||
|
||||
const { handAccuracy } = score.additionalData;
|
||||
const scoreImprovement = score.additionalData.scoreImprovement;
|
||||
return (
|
||||
<Tooltip display={"Right Hand Accuracy"}>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p>{handAccuracy.right.toFixed(2)}</p>
|
||||
{scoreImprovement && renderChange(scoreImprovement.handAccuracy.right, false, num => num.toFixed(2))}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
@ -144,7 +168,7 @@ type Props = {
|
||||
|
||||
export default function ScoreStats({ score, leaderboard }: Props) {
|
||||
return (
|
||||
<div className={`grid grid-cols-3 grid-rows-2 gap-1 ml-0 lg:ml-2 h-[64px]`}>
|
||||
<div className={`grid grid-cols-3 grid-rows-2 gap-1 ml-0 lg:ml-2 `}>
|
||||
<ScoreBadges badges={badges} score={score} leaderboard={leaderboard} />
|
||||
</div>
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ export default function StatValue({ name, color, value }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex min-w-16 gap-2 h-[28px] p-1 items-center justify-center rounded-md text-sm cursor-default",
|
||||
"flex min-w-16 gap-2 h-full p-1 items-center justify-center rounded-md text-sm cursor-default",
|
||||
color ? color : "bg-accent"
|
||||
)}
|
||||
style={{
|
||||
|
Reference in New Issue
Block a user