From c16e7bd128102ea034d3a5caa898949ed4034932 Mon Sep 17 00:00:00 2001 From: Lee Date: Sun, 16 Feb 2025 20:00:14 +0000 Subject: [PATCH] update score acc changer to use pp gain from current score not a fresh score --- .../src/controller/player.controller.ts | 4 +- .../backend/src/service/player.service.ts | 39 +++++++++++++--- .../response/player-ranked-pps-response.ts | 4 +- .../common/src/service/impl/scoresaber.ts | 14 +++--- projects/common/src/utils/scoresaber.util.ts | 13 ++++++ .../score/button/score-editor-button.tsx | 45 ++++++++++++++----- 6 files changed, 89 insertions(+), 30 deletions(-) diff --git a/projects/backend/src/controller/player.controller.ts b/projects/backend/src/controller/player.controller.ts index aa0756f9..b30e213c 100644 --- a/projects/backend/src/controller/player.controller.ts +++ b/projects/backend/src/controller/player.controller.ts @@ -267,8 +267,6 @@ export default class PlayerController { }: { params: { id: string }; }): Promise { - return { - pps: await PlayerService.getPlayerRankedPps(id), - }; + return await PlayerService.getPlayerRankedPps(id); } } diff --git a/projects/backend/src/service/player.service.ts b/projects/backend/src/service/player.service.ts index 9440b1db..d3fa6ee4 100644 --- a/projects/backend/src/service/player.service.ts +++ b/projects/backend/src/service/player.service.ts @@ -39,6 +39,8 @@ import { logNewTrackedPlayer } from "../common/embds"; import CacheService, { ServiceCache } from "./cache.service"; import { ScoreService } from "./score/score.service"; import ScoreSaberService from "./scoresaber.service"; +import { PlayerRankedPpsResponse } from "@ssr/common/response/player-ranked-pps-response"; +import { updateScoreWeights } from "@ssr/common/utils/scoresaber.util"; const accountCreationLock: { [id: string]: Promise } = {}; @@ -205,17 +207,32 @@ export class PlayerService { * @param playerId the player's id * @returns the ranked pp scores */ - public static async getPlayerRankedPps(playerId: string): Promise { + public static async getPlayerRankedPps(playerId: string): Promise { await this.ensurePlayerExists(playerId); const playerScores = await ScoreService.getPlayerScores(playerId, { ranked: true, sort: "pp", - projection: { pp: 1 }, + projection: { pp: 1, weight: 1, scoreId: 1 }, includeLeaderboard: false, }); - return playerScores.map(score => score.score.pp); + if (playerScores.length === 0) { + return { + scores: [], + }; + } + + const scores = playerScores.map(score => ({ + pp: score.score.pp, + weight: score.score.weight, + scoreId: score.score.scoreId, + })); + updateScoreWeights(scores); + + return { + scores, + }; } /** @@ -230,13 +247,18 @@ export class PlayerService { ): Promise { await this.ensurePlayerExists(playerId); const scoresPps = await this.getPlayerRankedPps(playerId); - if (scoresPps.length === 0) { + if (scoresPps.scores.length === 0) { return [0]; } const boundaries: number[] = []; for (let i = 1; i < boundary + 1; i++) { - boundaries.push(scoresaberService.calcPpBoundary(scoresPps, i)); + boundaries.push( + scoresaberService.calcPpBoundary( + scoresPps.scores.map(score => score.pp), + i + ) + ); } return boundaries; } @@ -253,11 +275,14 @@ export class PlayerService { ): Promise { await this.ensurePlayerExists(playerId); const scoresPps = await this.getPlayerRankedPps(playerId); - if (scoresPps.length === 0) { + if (scoresPps.scores.length === 0) { return 0; } - return scoresaberService.getPpBoundaryForRawPp(scoresPps, boundary); + return scoresaberService.getPpBoundaryForRawPp( + scoresPps.scores.map(score => score.pp), + boundary + ); } /** diff --git a/projects/common/src/response/player-ranked-pps-response.ts b/projects/common/src/response/player-ranked-pps-response.ts index 9e8a50c8..1f43e147 100644 --- a/projects/common/src/response/player-ranked-pps-response.ts +++ b/projects/common/src/response/player-ranked-pps-response.ts @@ -1,3 +1,5 @@ +import { ScoreSaberScore } from "../model/score/impl/scoresaber-score"; + export type PlayerRankedPpsResponse = { - pps: number[]; + scores: Pick[]; }; diff --git a/projects/common/src/service/impl/scoresaber.ts b/projects/common/src/service/impl/scoresaber.ts index 15df0cd8..ab67b0e5 100644 --- a/projects/common/src/service/impl/scoresaber.ts +++ b/projects/common/src/service/impl/scoresaber.ts @@ -43,10 +43,10 @@ const SEARCH_LEADERBOARDS_ENDPOINT = `${API_BASE}/leaderboards?search=:query`; */ const RANKING_REQUESTS_ENDPOINT = `${API_BASE}/ranking/requests/:query`; -const WEIGHT_COEFFICIENT = 0.965; -const STAR_MULTIPLIER = 42.117208413; - class ScoreSaberService extends Service { + readonly WEIGHT_COEFFICIENT = 0.965; + readonly STAR_MULTIPLIER = 42.117208413; + private curvePoints = [ new CurvePoint(0, 0), new CurvePoint(0.6, 0.18223233667439062), @@ -474,7 +474,7 @@ class ScoreSaberService extends Service { if (accuracy <= 1) { accuracy *= 100; // Convert the accuracy to a percentage } - const pp = stars * STAR_MULTIPLIER; // Calculate base PP value + const pp = stars * this.STAR_MULTIPLIER; // Calculate base PP value return this.getModifier(accuracy) * pp; // Calculate and return final PP value } @@ -492,7 +492,7 @@ class ScoreSaberService extends Service { // 0.965^idx * rawPpToFind = expected + oldBottomPp - newBottomPp; // rawPpToFind = (expected + oldBottomPp - newBottomPp) / 0.965^idx; - return (expected + oldBottomPp - newBottomPp) / Math.pow(WEIGHT_COEFFICIENT, idx); + return (expected + oldBottomPp - newBottomPp) / Math.pow(this.WEIGHT_COEFFICIENT, idx); } /** @@ -504,9 +504,9 @@ class ScoreSaberService extends Service { * @returns the total amount of weighted pp * @private */ - private getTotalWeightedPp(ppArray: Array, startIdx = 0) { + public getTotalWeightedPp(ppArray: Array, startIdx = 0) { return ppArray.reduce( - (cumulative, pp, idx) => cumulative + Math.pow(WEIGHT_COEFFICIENT, idx + startIdx) * pp, + (cumulative, pp, idx) => cumulative + Math.pow(this.WEIGHT_COEFFICIENT, idx + startIdx) * pp, 0 ); } diff --git a/projects/common/src/utils/scoresaber.util.ts b/projects/common/src/utils/scoresaber.util.ts index 59f6a73a..f8dc682f 100644 --- a/projects/common/src/utils/scoresaber.util.ts +++ b/projects/common/src/utils/scoresaber.util.ts @@ -1,3 +1,5 @@ +import { ScoreSaberScore } from "src/model/score/impl/scoresaber-score"; +import { scoresaberService } from "src/service/impl/scoresaber"; import { HMD } from "../hmds"; import ScoreSaberPlayer from "../player/impl/scoresaber-player"; import { MapDifficulty } from "../score/map-difficulty"; @@ -131,3 +133,14 @@ export function getScoreSaberAvatar( ): string { return `https://cdn.scoresaber.com/avatars/${player.id}.jpg`; } + +/** + * Updates the weights of the scores + * + * @param scores the scores + */ +export function updateScoreWeights(scores: Pick[]) { + for (let i = 0; i < scores.length; i++) { + scores[i].weight = Math.pow(scoresaberService.WEIGHT_COEFFICIENT, i); + } +} diff --git a/projects/website/src/components/score/button/score-editor-button.tsx b/projects/website/src/components/score/button/score-editor-button.tsx index 4c914823..8d705bea 100644 --- a/projects/website/src/components/score/button/score-editor-button.tsx +++ b/projects/website/src/components/score/button/score-editor-button.tsx @@ -6,9 +6,10 @@ import { useIsMobile } from "@/hooks/use-is-mobile"; import { ScoreSaberLeaderboard } from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard"; import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; +import { updateScoreWeights } from "@ssr/common/utils/scoresaber.util"; import { ssrApi } from "@ssr/common/utils/ssr-api"; import { useQuery } from "@tanstack/react-query"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { FaCog, FaUndo } from "react-icons/fa"; type ScoreEditorButtonProps = { @@ -33,6 +34,9 @@ export default function ScoreEditorButton({ queryFn: () => ssrApi.getPlayerRankedPps(score.playerId), }); + const [modifiedScores, setModifiedScores] = + useState[]>(); + 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; @@ -41,6 +45,23 @@ export default function ScoreEditorButton({ score: newBaseScore, }); setNewAccuracy(newAccuracy); + + if (rankedPps) { + let newModifiedScores = [...rankedPps.scores]; + for (let i = 0; i < newModifiedScores.length; i++) { + const modifiedScore = newModifiedScores[i]; + if (score.scoreId == modifiedScore.scoreId) { + newModifiedScores[i] = { + ...modifiedScore, + pp: scoresaberService.getPp(leaderboard.stars, newAccuracy), + }; + } + } + newModifiedScores = newModifiedScores.sort((a, b) => b.pp - a.pp); + updateScoreWeights(newModifiedScores); + + setModifiedScores(newModifiedScores); + } }; const handleSliderReset = () => { @@ -51,20 +72,20 @@ export default function ScoreEditorButton({ setNewAccuracy(accuracy); }; - const ppGain = rankedPps - ? scoresaberService.getPpBoundaryForRawPp( - rankedPps.pps, - scoresaberService.getPp(leaderboard.stars, newAccuracy) - ) - : 0; + const ppGain = useMemo(() => { + if (!rankedPps || !modifiedScores) { + return 0; + } + + return ( + scoresaberService.getTotalWeightedPp(modifiedScores.map(score => score.pp)) - + scoresaberService.getTotalWeightedPp(rankedPps.scores.map(score => score.pp)) + ); + }, [modifiedScores, rankedPps]); return (
- { - handleSliderReset(); - }} - > + handleSliderReset()}> Edit Score Accuracy

}>