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 { fetchWithCache } from "../common/cache.util";
|
||||||
import { PlayerDocument, PlayerModel } from "@ssr/common/model/player";
|
import { PlayerDocument, PlayerModel } from "@ssr/common/model/player";
|
||||||
import { BeatLeaderScoreToken } from "@ssr/common/types/token/beatleader/beatleader-score-token";
|
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({
|
const playerScoresCache = new SSRCache({
|
||||||
ttl: 1000 * 60, // 1 minute
|
ttl: 1000 * 60, // 1 minute
|
||||||
@ -167,22 +170,65 @@ export class ScoreService {
|
|||||||
return;
|
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 difficulty = leaderboard.difficulty;
|
||||||
const difficultyKey = `${difficulty.difficultyName.replace("Plus", "+")}-${difficulty.modeName}`;
|
const difficultyKey = `${difficulty.difficultyName.replace("Plus", "+")}-${difficulty.modeName}`;
|
||||||
await AdditionalScoreDataModel.create({
|
const rawScoreImprovement = score.scoreImprovement;
|
||||||
|
const data = {
|
||||||
playerId: playerId,
|
playerId: playerId,
|
||||||
songHash: leaderboard.song.hash,
|
songHash: leaderboard.song.hash.toUpperCase(),
|
||||||
songDifficulty: difficultyKey,
|
songDifficulty: difficultyKey,
|
||||||
songScore: score.baseScore,
|
songScore: score.baseScore,
|
||||||
bombCuts: score.bombCuts,
|
misses: {
|
||||||
wallsHit: score.wallsHit,
|
misses: score.missedNotes + score.badCuts,
|
||||||
|
missedNotes: score.missedNotes,
|
||||||
|
bombCuts: score.bombCuts,
|
||||||
|
badCuts: score.badCuts,
|
||||||
|
wallsHit: score.wallsHit,
|
||||||
|
},
|
||||||
pauses: score.pauses,
|
pauses: score.pauses,
|
||||||
fcAccuracy: score.fcAccuracy * 100,
|
fcAccuracy: score.fcAccuracy * 100,
|
||||||
|
fullCombo: score.fullCombo,
|
||||||
handAccuracy: {
|
handAccuracy: {
|
||||||
left: score.accLeft,
|
left: score.accLeft,
|
||||||
right: score.accRight,
|
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(
|
console.log(
|
||||||
`Tracked additional score data for "${scorePlayer.name}"(${playerId}), difficulty: ${difficultyKey}, score: ${score.baseScore}`
|
`Tracked additional score data for "${scorePlayer.name}"(${playerId}), difficulty: ${difficultyKey}, score: ${score.baseScore}`
|
||||||
);
|
);
|
||||||
@ -205,7 +251,7 @@ export class ScoreService {
|
|||||||
): Promise<AdditionalScoreData | undefined> {
|
): Promise<AdditionalScoreData | undefined> {
|
||||||
const additionalData = await AdditionalScoreDataModel.findOne({
|
const additionalData = await AdditionalScoreDataModel.findOne({
|
||||||
playerId: playerId,
|
playerId: playerId,
|
||||||
songHash: songHash,
|
songHash: songHash.toUpperCase(),
|
||||||
songDifficulty: songDifficulty,
|
songDifficulty: songDifficulty,
|
||||||
songScore: songScore,
|
songScore: songScore,
|
||||||
});
|
});
|
||||||
@ -232,7 +278,6 @@ export class ScoreService {
|
|||||||
sort: string,
|
sort: string,
|
||||||
search?: string
|
search?: string
|
||||||
): Promise<PlayerScoresResponse<unknown, unknown> | undefined> {
|
): Promise<PlayerScoresResponse<unknown, unknown> | undefined> {
|
||||||
console.log("hi");
|
|
||||||
return fetchWithCache(
|
return fetchWithCache(
|
||||||
playerScoresCache,
|
playerScoresCache,
|
||||||
`player-scores-${leaderboardName}-${id}-${page}-${sort}-${search}`,
|
`player-scores-${leaderboardName}-${id}-${page}-${sort}-${search}`,
|
||||||
@ -275,7 +320,6 @@ export class ScoreService {
|
|||||||
`${tokenLeaderboard.difficulty.difficulty}-${tokenLeaderboard.difficulty.gameMode}`,
|
`${tokenLeaderboard.difficulty.difficulty}-${tokenLeaderboard.difficulty.gameMode}`,
|
||||||
score.score
|
score.score
|
||||||
);
|
);
|
||||||
console.log("additionalData", additionalData);
|
|
||||||
if (additionalData !== undefined) {
|
if (additionalData !== undefined) {
|
||||||
score.additionalData = additionalData;
|
score.additionalData = additionalData;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardTo
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: token.id,
|
id: token.id,
|
||||||
songHash: token.songHash,
|
songHash: token.songHash.toUpperCase(),
|
||||||
songName: token.songName,
|
songName: token.songName,
|
||||||
songSubName: token.songSubName,
|
songSubName: token.songSubName,
|
||||||
songAuthorName: token.songAuthorName,
|
songAuthorName: token.songAuthorName,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose";
|
import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose";
|
||||||
import { Document } from "mongoose";
|
import { Document } from "mongoose";
|
||||||
|
import { HandAccuracy } from "./hand-accuracy";
|
||||||
|
import { Misses } from "./misses";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The model for a BeatSaver map.
|
* The model for a BeatSaver map.
|
||||||
@ -47,18 +49,8 @@ export class AdditionalScoreData {
|
|||||||
@prop({ required: true, index: true })
|
@prop({ required: true, index: true })
|
||||||
public songScore!: number;
|
public songScore!: number;
|
||||||
|
|
||||||
/**
|
// Above data is only so we can fetch it
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount of pauses in the play.
|
* The amount of pauses in the play.
|
||||||
@ -66,28 +58,61 @@ export class AdditionalScoreData {
|
|||||||
@prop({ required: false })
|
@prop({ required: false })
|
||||||
public pauses!: number;
|
public pauses!: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The miss data for the play.
|
||||||
|
*/
|
||||||
|
@prop({ required: false, _id: false })
|
||||||
|
public misses!: Misses;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The hand accuracy for each hand.
|
* The hand accuracy for each hand.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@prop({ required: false })
|
@prop({ required: false, _id: false })
|
||||||
public handAccuracy!: {
|
public handAccuracy!: HandAccuracy;
|
||||||
/**
|
|
||||||
* The left hand accuracy.
|
|
||||||
*/
|
|
||||||
left: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The right hand accuracy.
|
|
||||||
*/
|
|
||||||
right: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The full combo accuracy of the play.
|
* The full combo accuracy of the play.
|
||||||
*/
|
*/
|
||||||
@prop({ required: true })
|
@prop({ required: true })
|
||||||
public fcAccuracy!: number;
|
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;
|
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 { Modifier } from "./modifier";
|
||||||
import { Leaderboards } from "../leaderboard";
|
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 {
|
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 { formatNumberWithCommas } from "@ssr/common/utils/number-utils";
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Tooltip from "@/components/tooltip";
|
|
||||||
import { ScoreBadgeProps } from "@/components/score/badges/badge-props";
|
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 & {
|
type ScoreMissesBadgeProps = ScoreBadgeProps & {
|
||||||
/**
|
/**
|
||||||
@ -12,32 +13,65 @@ type ScoreMissesBadgeProps = ScoreBadgeProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ScoreMissesBadge({ score, hideXMark }: ScoreMissesBadgeProps) {
|
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 (
|
return (
|
||||||
<Tooltip
|
<div className="flex flex-col justify-center items-center">
|
||||||
display={
|
<ScoreMissesTooltip
|
||||||
<div className="flex flex-col">
|
missedNotes={score.missedNotes}
|
||||||
{!score.fullCombo ? (
|
badCuts={score.badCuts}
|
||||||
<>
|
bombCuts={misses?.bombCuts}
|
||||||
<p className="font-semibold">Misses</p>
|
wallsHit={misses?.wallsHit}
|
||||||
<p>Missed Notes: {formatNumberWithCommas(score.missedNotes)}</p>
|
fullCombo={score.fullCombo}
|
||||||
<p>Bad Cuts: {formatNumberWithCommas(score.badCuts)}</p>
|
>
|
||||||
{score.additionalData && (
|
<div className="flex gap-1 items-center">
|
||||||
<>
|
<p>{score.fullCombo ? <span className="text-green-400">FC</span> : formatNumberWithCommas(score.misses)}</p>
|
||||||
<p>Bomb Cuts: {formatNumberWithCommas(score.additionalData.bombCuts)}</p>
|
{!hideXMark && <XMarkIcon className={clsx("w-5 h-5", score.fullCombo ? "hidden" : "text-red-400")} />}
|
||||||
<p>Wall Hits: {formatNumberWithCommas(score.additionalData.wallsHit)}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p>Full Combo</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</ScoreMissesTooltip>
|
||||||
>
|
{additionalData && previousScoreMisses && scoreImprovement && misses && (
|
||||||
<div className="flex gap-1 items-center justify-center">
|
<div className="flex gap-2 items-center justify-center">
|
||||||
<p>{score.fullCombo ? <span className="text-green-400">FC</span> : formatNumberWithCommas(score.misses)}</p>
|
<ScoreMissesTooltip
|
||||||
{!hideXMark && <XMarkIcon className={clsx("w-5 h-5", score.fullCombo ? "hidden" : "text-red-400")} />}
|
missedNotes={previousScoreMisses.missedNotes}
|
||||||
</div>
|
badCuts={previousScoreMisses.badCuts}
|
||||||
</Tooltip>
|
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();
|
const { toast } = useToast();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex justify-end gap-2 h-[${alwaysSingleLine ? "32" : "64"}px]`}>
|
<div className={`flex justify-end gap-2 items-center`}>
|
||||||
<div
|
<div
|
||||||
className={`flex ${alwaysSingleLine ? "flex-nowrap" : "flex-wrap"} items-center lg:items-start justify-center lg:justify-end gap-1`}
|
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 ScoreMissesBadge from "@/components/score/badges/score-misses";
|
||||||
import { Modifier } from "@ssr/common/score/modifier";
|
import { Modifier } from "@ssr/common/score/modifier";
|
||||||
import { ScoreModifiers } from "@/components/score/score-modifiers";
|
import { ScoreModifiers } from "@/components/score/score-modifiers";
|
||||||
|
import { renderChange } from "@/common/change";
|
||||||
|
|
||||||
const badges: ScoreBadge[] = [
|
const badges: ScoreBadge[] = [
|
||||||
{
|
{
|
||||||
@ -48,6 +49,8 @@ const badges: ScoreBadge[] = [
|
|||||||
return getScoreBadgeFromAccuracy(acc).color;
|
return getScoreBadgeFromAccuracy(acc).color;
|
||||||
},
|
},
|
||||||
create: (score: ScoreSaberScore, leaderboard: ScoreSaberLeaderboard) => {
|
create: (score: ScoreSaberScore, leaderboard: ScoreSaberLeaderboard) => {
|
||||||
|
const scoreImprovement = score.additionalData?.scoreImprovement;
|
||||||
|
|
||||||
const acc = (score.score / leaderboard.maxScore) * 100;
|
const acc = (score.score / leaderboard.maxScore) * 100;
|
||||||
const fcAccuracy = score.additionalData?.fcAccuracy;
|
const fcAccuracy = score.additionalData?.fcAccuracy;
|
||||||
const scoreBadge = getScoreBadgeFromAccuracy(acc);
|
const scoreBadge = getScoreBadgeFromAccuracy(acc);
|
||||||
@ -83,9 +86,15 @@ const badges: ScoreBadge[] = [
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p className="cursor-default">
|
<div className="flex flex-col items-center justify-center cursor-default">
|
||||||
{acc.toFixed(2)}% {modCount > 0 && <ScoreModifiers type="simple" limit={1} score={score} />}
|
<p>
|
||||||
</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>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -94,7 +103,14 @@ const badges: ScoreBadge[] = [
|
|||||||
{
|
{
|
||||||
name: "Score",
|
name: "Score",
|
||||||
create: (score: ScoreSaberScore) => {
|
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) {
|
if (!score.additionalData) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { handAccuracy } = score.additionalData;
|
const { handAccuracy } = score.additionalData;
|
||||||
|
const scoreImprovement = score.additionalData.scoreImprovement;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip display={"Left Hand Accuracy"}>
|
<Tooltip display={"Left Hand Accuracy"}>
|
||||||
<p>{handAccuracy.left.toFixed(2)}</p>
|
<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>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -122,9 +142,13 @@ const badges: ScoreBadge[] = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { handAccuracy } = score.additionalData;
|
const { handAccuracy } = score.additionalData;
|
||||||
|
const scoreImprovement = score.additionalData.scoreImprovement;
|
||||||
return (
|
return (
|
||||||
<Tooltip display={"Right Hand Accuracy"}>
|
<Tooltip display={"Right Hand Accuracy"}>
|
||||||
<p>{handAccuracy.right.toFixed(2)}</p>
|
<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>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -144,7 +168,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function ScoreStats({ score, leaderboard }: Props) {
|
export default function ScoreStats({ score, leaderboard }: Props) {
|
||||||
return (
|
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} />
|
<ScoreBadges badges={badges} score={score} leaderboard={leaderboard} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -21,7 +21,7 @@ export default function StatValue({ name, color, value }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
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"
|
color ? color : "bg-accent"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
Reference in New Issue
Block a user