From 98e8273c0741c5ba5df1c06a353f51b22490694a Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 12 Oct 2024 07:21:55 +0100 Subject: [PATCH] add score "edit" mode --- bun.lockb | Bin 295456 -> 302536 bytes .../common/src/service/impl/scoresaber.ts | 105 ++++++++++++++++-- projects/common/src/utils/curve-point.ts | 14 +++ projects/common/src/utils/math-utils.ts | 29 +++++ projects/website/package.json | 2 + .../components/score/leaderboard-button.tsx | 25 ----- .../src/components/score/score-buttons.tsx | 47 ++++++-- .../components/score/score-editor-button.tsx | 70 ++++++++++++ .../src/components/score/score-stats.tsx | 2 +- .../website/src/components/score/score.tsx | 29 +++-- .../website/src/components/ui/popover.tsx | 32 ++++++ projects/website/src/components/ui/slider.tsx | 24 ++++ 12 files changed, 328 insertions(+), 51 deletions(-) create mode 100644 projects/common/src/utils/curve-point.ts create mode 100644 projects/common/src/utils/math-utils.ts delete mode 100644 projects/website/src/components/score/leaderboard-button.tsx create mode 100644 projects/website/src/components/score/score-editor-button.tsx create mode 100644 projects/website/src/components/ui/popover.tsx create mode 100644 projects/website/src/components/ui/slider.tsx diff --git a/bun.lockb b/bun.lockb index f38cf1d50a793fcb1f34b78addbcb1f8230db0b3..c8dc2993204858108418eb905f632700c0592181 100755 GIT binary patch delta 52195 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 diff --git a/projects/common/src/service/impl/scoresaber.ts b/projects/common/src/service/impl/scoresaber.ts index 27c8295..3f3640f 100644 --- a/projects/common/src/service/impl/scoresaber.ts +++ b/projects/common/src/service/impl/scoresaber.ts @@ -6,6 +6,8 @@ import { ScoreSort } from "../../types/score/score-sort"; import ScoreSaberPlayerScoresPageToken from "../../types/token/scoresaber/score-saber-player-scores-page-token"; import ScoreSaberLeaderboardToken from "../../types/token/scoresaber/score-saber-leaderboard-token"; import ScoreSaberLeaderboardScoresPageToken from "../../types/token/scoresaber/score-saber-leaderboard-scores-page-token"; +import { clamp, lerp } from "../../utils/math-utils"; +import { CurvePoint } from "../../utils/curve-point"; const API_BASE = "https://scoresaber.com/api"; @@ -24,7 +26,49 @@ const LOOKUP_PLAYER_SCORES_ENDPOINT = `${API_BASE}/player/:id/scores?limit=:limi const LOOKUP_LEADERBOARD_ENDPOINT = `${API_BASE}/leaderboard/by-id/:id/info`; const LOOKUP_LEADERBOARD_SCORES_ENDPOINT = `${API_BASE}/leaderboard/by-id/:id/scores?page=:page`; +const STAR_MULTIPLIER = 42.117208413; + class ScoreSaberService extends Service { + private curvePoints = [ + new CurvePoint(0, 0), + new CurvePoint(0.6, 0.18223233667439062), + new CurvePoint(0.65, 0.5866010012767576), + new CurvePoint(0.7, 0.6125565959114954), + new CurvePoint(0.75, 0.6451808210101443), + new CurvePoint(0.8, 0.6872268862950283), + new CurvePoint(0.825, 0.7150465663454271), + new CurvePoint(0.85, 0.7462290664143185), + new CurvePoint(0.875, 0.7816934560296046), + new CurvePoint(0.9, 0.825756123560842), + new CurvePoint(0.91, 0.8488375988124467), + new CurvePoint(0.92, 0.8728710341448851), + new CurvePoint(0.93, 0.9039994071865736), + new CurvePoint(0.94, 0.9417362980580238), + new CurvePoint(0.95, 1), + new CurvePoint(0.955, 1.0388633331418984), + new CurvePoint(0.96, 1.0871883573850478), + new CurvePoint(0.965, 1.1552120359501035), + new CurvePoint(0.97, 1.2485807759957321), + new CurvePoint(0.9725, 1.3090333065057616), + new CurvePoint(0.975, 1.3807102743105126), + new CurvePoint(0.9775, 1.4664726399289512), + new CurvePoint(0.98, 1.5702410055532239), + new CurvePoint(0.9825, 1.697536248647543), + new CurvePoint(0.985, 1.8563887693647105), + new CurvePoint(0.9875, 2.058947159052738), + new CurvePoint(0.99, 2.324506282149922), + new CurvePoint(0.99125, 2.4902905794106913), + new CurvePoint(0.9925, 2.685667856592722), + new CurvePoint(0.99375, 2.9190155639254955), + new CurvePoint(0.995, 3.2022017597337955), + new CurvePoint(0.99625, 3.5526145337555373), + new CurvePoint(0.9975, 3.996793606763322), + new CurvePoint(0.99825, 4.325027383589547), + new CurvePoint(0.999, 4.715470646416203), + new CurvePoint(0.9995, 5.019543595874787), + new CurvePoint(1, 5.367394282890631), + ]; + constructor() { super("ScoreSaber"); } @@ -35,7 +79,7 @@ class ScoreSaberService extends Service { * @param query the query to search for * @returns the players that match the query, or undefined if no players were found */ - async searchPlayers(query: string): Promise { + public async searchPlayers(query: string): Promise { const before = performance.now(); this.log(`Searching for players matching "${query}"...`); const results = await this.fetch(SEARCH_PLAYERS_ENDPOINT.replace(":query", query)); @@ -56,7 +100,7 @@ class ScoreSaberService extends Service { * @param playerId the ID of the player to look up * @returns the player that matches the ID, or undefined */ - async lookupPlayer(playerId: string): Promise { + public async lookupPlayer(playerId: string): Promise { const before = performance.now(); this.log(`Looking up player "${playerId}"...`); const token = await this.fetch(LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId)); @@ -73,7 +117,7 @@ class ScoreSaberService extends Service { * @param page the page to get players for * @returns the players on the page, or undefined */ - async lookupPlayers(page: number): Promise { + public async lookupPlayers(page: number): Promise { const before = performance.now(); this.log(`Looking up players on page "${page}"...`); const response = await this.fetch( @@ -93,7 +137,7 @@ class ScoreSaberService extends Service { * @param country the country to get players for * @returns the players on the page, or undefined */ - async lookupPlayersByCountry(page: number, country: string): Promise { + public async lookupPlayersByCountry(page: number, country: string): Promise { const before = performance.now(); this.log(`Looking up players on page "${page}" for country "${country}"...`); const response = await this.fetch( @@ -115,7 +159,7 @@ class ScoreSaberService extends Service { * @param search * @returns the scores of the player, or undefined */ - async lookupPlayerScores({ + public async lookupPlayerScores({ playerId, sort, page, @@ -151,7 +195,7 @@ class ScoreSaberService extends Service { * * @param leaderboardId the ID of the leaderboard to look up */ - async lookupLeaderboard(leaderboardId: string): Promise { + public async lookupLeaderboard(leaderboardId: string): Promise { const before = performance.now(); this.log(`Looking up leaderboard "${leaderboardId}"...`); const response = await this.fetch( @@ -171,7 +215,7 @@ class ScoreSaberService extends Service { * @param page the page to get scores for * @returns the scores of the leaderboard, or undefined */ - async lookupLeaderboardScores( + public async lookupLeaderboardScores( leaderboardId: string, page: number ): Promise { @@ -188,6 +232,53 @@ class ScoreSaberService extends Service { ); return response; } + + /** + * Gets the modifier for the given accuracy. + * + * @param accuracy The accuracy. + * @return The modifier. + */ + public getModifier(accuracy: number): number { + accuracy = clamp(accuracy, 0, 100) / 100; // Normalize accuracy to a range of [0, 1] + + if (accuracy <= 0) { + return 0; + } + + if (accuracy >= 1) { + return this.curvePoints[this.curvePoints.length - 1].getMultiplier(); + } + + for (let i = 0; i < this.curvePoints.length - 1; i++) { + const point = this.curvePoints[i]; + const nextPoint = this.curvePoints[i + 1]; + if (accuracy >= point.getAcc() && accuracy <= nextPoint.getAcc()) { + return lerp( + point.getMultiplier(), + nextPoint.getMultiplier(), + (accuracy - point.getAcc()) / (nextPoint.getAcc() - point.getAcc()) + ); + } + } + + return 0; + } + + /** + * Gets the performance points (PP) based on stars and accuracy. + * + * @param stars The star count. + * @param accuracy The accuracy. + * @returns The calculated PP. + */ + public getPp(stars: number, accuracy: number): number { + if (accuracy <= 1) { + accuracy *= 100; // Convert the accuracy to a percentage + } + const pp = stars * STAR_MULTIPLIER; // Calculate base PP value + return this.getModifier(accuracy) * pp; // Calculate and return final PP value + } } export const scoresaberService = new ScoreSaberService(); diff --git a/projects/common/src/utils/curve-point.ts b/projects/common/src/utils/curve-point.ts new file mode 100644 index 0000000..bc05e78 --- /dev/null +++ b/projects/common/src/utils/curve-point.ts @@ -0,0 +1,14 @@ +export class CurvePoint { + constructor( + private acc: number, + private multiplier: number + ) {} + + getAcc(): number { + return this.acc; + } + + getMultiplier(): number { + return this.multiplier; + } +} diff --git a/projects/common/src/utils/math-utils.ts b/projects/common/src/utils/math-utils.ts new file mode 100644 index 0000000..29ff53a --- /dev/null +++ b/projects/common/src/utils/math-utils.ts @@ -0,0 +1,29 @@ +/** + * Clamps a value between two values. + * + * @param value the value + * @param min the minimum + * @param max the maximum + */ +export function clamp(value: number, min: number, max: number) { + if (min !== null && value < min) { + return min; + } + + if (max !== null && value > max) { + return max; + } + + return value; +} + +/** + * Lerps between two values. + * + * @param v0 value 0 + * @param v1 value 1 + * @param t the amount to lerp + */ +export function lerp(v0: number, v1: number, t: number) { + return v0 + t * (v1 - v0); +} diff --git a/projects/website/package.json b/projects/website/package.json index e57194f..1bee849 100644 --- a/projects/website/package.json +++ b/projects/website/package.json @@ -15,7 +15,9 @@ "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-tooltip": "^1.1.2", diff --git a/projects/website/src/components/score/leaderboard-button.tsx b/projects/website/src/components/score/leaderboard-button.tsx deleted file mode 100644 index 6e2d1fd..0000000 --- a/projects/website/src/components/score/leaderboard-button.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { ArrowDownIcon } from "@heroicons/react/24/solid"; -import clsx from "clsx"; -import { Dispatch, SetStateAction } from "react"; - -type Props = { - isLeaderboardExpanded: boolean; - setIsLeaderboardExpanded: Dispatch>; -}; - -export default function LeaderboardButton({ isLeaderboardExpanded, setIsLeaderboardExpanded }: Props) { - return ( -
- -
- ); -} diff --git a/projects/website/src/components/score/score-buttons.tsx b/projects/website/src/components/score/score-buttons.tsx index 1a214ee..8a547d7 100644 --- a/projects/website/src/components/score/score-buttons.tsx +++ b/projects/website/src/components/score/score-buttons.tsx @@ -5,27 +5,34 @@ import { songNameToYouTubeLink } from "@/common/youtube-utils"; import BeatSaverLogo from "@/components/logos/beatsaver-logo"; import YouTubeLogo from "@/components/logos/youtube-logo"; import { useToast } from "@/hooks/use-toast"; -import { Dispatch, SetStateAction } from "react"; -import LeaderboardButton from "./leaderboard-button"; +import { useState } from "react"; import ScoreButton from "./score-button"; import { copyToClipboard } from "@/common/browser-utils"; import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; +import { Button } from "@/components/ui/button"; +import { ArrowDownIcon } from "@heroicons/react/24/solid"; +import clsx from "clsx"; +import ScoreEditorButton from "@/components/score/score-editor-button"; +import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; type Props = { + score: ScoreSaberScoreToken; leaderboard: ScoreSaberLeaderboardToken; beatSaverMap?: BeatSaverMap; alwaysSingleLine?: boolean; - isLeaderboardExpanded?: boolean; - setIsLeaderboardExpanded?: Dispatch>; + setIsLeaderboardExpanded: (isExpanded: boolean) => void; + setScore: (score: ScoreSaberScoreToken) => void; }; export default function ScoreButtons({ + score, leaderboard, beatSaverMap, alwaysSingleLine, - isLeaderboardExpanded, setIsLeaderboardExpanded, + setScore, }: Props) { + const [leaderboardExpanded, setLeaderboardExpanded] = useState(false); const { toast } = useToast(); return ( @@ -74,12 +81,30 @@ export default function ScoreButtons({ - {isLeaderboardExpanded != undefined && setIsLeaderboardExpanded != undefined && ( - - )} +
+ {/* Edit score button */} + {score && leaderboard && setScore && ( + + )} + + {/* View Leaderboard button */} + {leaderboardExpanded != undefined && setIsLeaderboardExpanded != undefined && ( +
+ +
+ )} +
); } diff --git a/projects/website/src/components/score/score-editor-button.tsx b/projects/website/src/components/score/score-editor-button.tsx new file mode 100644 index 0000000..bdeeb8a --- /dev/null +++ b/projects/website/src/components/score/score-editor-button.tsx @@ -0,0 +1,70 @@ +import { Button } from "@/components/ui/button"; +import { CogIcon } from "@heroicons/react/24/solid"; +import clsx from "clsx"; +import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; +import { useState } from "react"; +import { Slider } from "@/components/ui/slider"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { ResetIcon } from "@radix-ui/react-icons"; +import Tooltip from "@/components/tooltip"; + +type ScoreEditorButtonProps = { + score: ScoreSaberScoreToken; + leaderboard: ScoreSaberLeaderboardToken; + setScore: (score: ScoreSaberScoreToken) => void; +}; + +export default function ScoreEditorButton({ score, leaderboard, setScore }: ScoreEditorButtonProps) { + const [isScoreEditMode, setIsScoreEditMode] = useState(false); + + const maxScore = leaderboard.maxScore || 1; // Use 1 to prevent division by zero + const accuracy = (score.baseScore / maxScore) * 100; + + const handleSliderChange = (value: number[]) => { + const newAccuracy = Math.max(0, Math.min(value[0], 100)); // Ensure the accuracy stays within 0-100 + const newBaseScore = (newAccuracy / 100) * maxScore; + setScore({ + ...score, + baseScore: newBaseScore, + }); + }; + + const handleSliderReset = () => { + setScore({ + ...score, + baseScore: (accuracy / 100) * maxScore, + }); + }; + + return ( +
+ { + setIsScoreEditMode(open); + handleSliderReset(); + }} + > + + + + +
+

Accuracy Changer

+ {/* Accuracy Slider */} + + + Set accuracy to score accuracy

}> + {/* Reset Button (Changes accuracy back to the original accuracy) */} + +
+
+
+
+
+ ); +} diff --git a/projects/website/src/components/score/score-stats.tsx b/projects/website/src/components/score/score-stats.tsx index bac0969..5191a5a 100644 --- a/projects/website/src/components/score/score-stats.tsx +++ b/projects/website/src/components/score/score-stats.tsx @@ -73,7 +73,7 @@ const badges: ScoreBadge[] = [ { name: "Score", create: (score: ScoreSaberScoreToken) => { - return `${formatNumberWithCommas(score.baseScore)}`; + return `${formatNumberWithCommas(Number(score.baseScore.toFixed(0)))}`; }, }, { diff --git a/projects/website/src/components/score/score.tsx b/projects/website/src/components/score/score.tsx index 8d74639..fa13631 100644 --- a/projects/website/src/components/score/score.tsx +++ b/projects/website/src/components/score/score.tsx @@ -12,6 +12,7 @@ import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token"; import { lookupBeatSaverMap } from "@/common/beatsaver-utils"; import { getPageFromRank } from "@ssr/common/utils/utils"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; type Props = { /** @@ -27,33 +28,47 @@ type Props = { export default function Score({ player, playerScore }: Props) { const { score, leaderboard } = playerScore; + const [baseScore, setBaseScore] = useState(score.baseScore); const [beatSaverMap, setBeatSaverMap] = useState(); const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false); const fetchBeatSaverData = useCallback(async () => { - const beatSaverMap = await lookupBeatSaverMap(leaderboard.songHash); - setBeatSaverMap(beatSaverMap); + const beatSaverMapData = await lookupBeatSaverMap(leaderboard.songHash); + setBeatSaverMap(beatSaverMapData); }, [leaderboard.songHash]); useEffect(() => { fetchBeatSaverData(); }, [fetchBeatSaverData]); + const accuracy = (baseScore / leaderboard.maxScore) * 100; + const pp = scoresaberService.getPp(leaderboard.stars, accuracy); return (
-
+ {/* Score Info */} +
{ + setBaseScore(score.baseScore); + }} + /> + -
+ + {/* Leaderboard */} {isLeaderboardExpanded && ( , + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/projects/website/src/components/ui/slider.tsx b/projects/website/src/components/ui/slider.tsx new file mode 100644 index 0000000..e1528bd --- /dev/null +++ b/projects/website/src/components/ui/slider.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; +import { cn } from "@/common/utils"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider };