[+] Chusan rating calculation

This commit is contained in:
Azalea
2024-12-26 19:37:55 -05:00
parent 038e76ed94
commit 8b90449970
3 changed files with 171 additions and 115 deletions

View File

@@ -4,7 +4,7 @@
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
import { DATA_HOST } from "../libs/config"; import { DATA_HOST } from "../libs/config";
import { t } from "../libs/i18n"; import { t } from "../libs/i18n";
import { type GameName, getMult, roundFloor } from "../libs/scoring"; import { type GameName, getMult, parseComposition, roundFloor } from "../libs/scoring";
import { coverNotFound } from "../libs/ui"; import { coverNotFound } from "../libs/ui";
import type { MusicMeta } from "../libs/generalTypes"; import type { MusicMeta } from "../libs/generalTypes";
import { tooltip } from "../libs/ui"; import { tooltip } from "../libs/ui";
@@ -14,47 +14,35 @@
export let meta: MusicMeta export let meta: MusicMeta
export let game: GameName export let game: GameName
let mapData = g.split(":").map(Number) // // mapData: [id, difficulty, score, rank]
let mult = getMult(mapData[3], game) // let mapData = g.split(":").map(Number)
let mapRank: number | undefined = meta?.notes?.[mapData[1] === 10 ? 0 : mapData[1]]?.lv // // mult: [score cutoff, rank multiplier, rank text]
const rounding = useLocalStorage("rounding", true); // let mult = getMult(mapData[3], game)
// let mapRank: number | undefined = meta?.notes?.[mapData[1] === 10 ? 0 : mapData[1]]?.lv
let gameIndexMap = { const p = parseComposition(g, meta, game)
'mai2': 3, const rounding = useLocalStorage("rounding", true)
'ongeki': 2, </script>
'chu3': 2
};
let gameIndex = gameIndexMap[game as keyof typeof gameIndexMap];
<div class="map-detail-container" transition:slide> <div class="map-detail-container" transition:slide>
<div class="scores"> <div class="scores">
<div> <div>
<div> <img src={p.img} alt="" on:error={coverNotFound} />
<div class="info"> <div class="info">
<div class="first-line"> <div class="first-line">
<div class="song-title">{meta?.name ?? t("UserHome.UnknownSong")}</div> <div class="song-title">{meta?.name ?? t("UserHome.UnknownSong")}</div>
<div class="song-title">{meta?.name ?? t("UserHome.UnknownSong")}</div> <span class={`lv level-${p.diffId === 10 ? 3 : p.diffId}`}>
<span class={`lv level-${mapData[1] === 10 ? 3 : mapData[1]}`}> { p.difficulty ?? '-' }
</span> </span>
</div> </div>
<div class="second-line"> <div class="second-line">
<div class="second-line"> <span class={`rank-${p.rank[0]}`}>
<span class={`rank-${getMult(mapData[gameIndex], game)[2].toString()[0]}`}> <span class="rank-text">{p.rank.replace("p", "+")}</span>
<span class="rank-num" use:tooltip={(p.score / 10000).toFixed(4)}>
<span class="rank-text">{("" + getMult(mapData[gameIndex], game)[2]).replace("p", "+")}</span> {rounding.value ? roundFloor(p.score, game, 1) : (p.score / 10000).toFixed(4)}%
<span class="rank-num" use:tooltip={(mapData[gameIndex] / 10000).toFixed(4)}>
{
rounding.value ?
roundFloor(mapData[gameIndex], game, 1) :
(mapData[gameIndex] / 10000).toFixed(4)
</span> </span>
</span> </span>
</span> {#if p.ratingChange !== undefined}
{#if game === 'mai2'} <span class="dx-change">{ p.ratingChange.toFixed(1) }</span>
<span class="dx-change">
{ mapRank ? Math.floor(mapRank * mult[1] * (Math.min(100.5, mapData[3] / 10000) / 100)) : '-' }
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -1,85 +1,151 @@
export type GameName = 'mai2' | 'chu3' | 'ongeki' | 'wacca' import { DATA_HOST } from "./config"
import type { MusicMeta } from "./generalTypes"
const multTable = {
'mai2': [ export type GameName = 'mai2' | 'chu3' | 'ongeki' | 'wacca'
[ 100.5, 22.4, 'SSSp' ],
[ 100.0, 21.6, 'SSS' ], const multTable = {
[ 99.5, 21.1, 'SSp' ], 'mai2': [
[ 99, 20.8, 'SS' ], [ 100.5, 22.4, 'SSSp' ],
[ 98, 20.3, 'Sp' ], [ 100.0, 21.6, 'SSS' ],
[ 97, 20, 'S' ], [ 99.5, 21.1, 'SSp' ],
[ 94, 16.8, 'AAA' ], [ 99, 20.8, 'SS' ],
[ 90, 15.2, 'AA' ], [ 98, 20.3, 'Sp' ],
[ 80, 13.6, 'A' ], [ 97, 20, 'S' ],
[ 75, 12, 'BBB' ], [ 94, 16.8, 'AAA' ],
[ 70, 11.2, 'BB' ], [ 90, 15.2, 'AA' ],
[ 60, 9.6, 'B' ], [ 80, 13.6, 'A' ],
[ 50, 8, 'C' ], [ 75, 12, 'BBB' ],
[ 40, 6.4, 'D' ], [ 70, 11.2, 'BB' ],
[ 30, 4.8, 'D' ], [ 60, 9.6, 'B' ],
[ 20, 3.2, 'D' ], [ 50, 8, 'C' ],
[ 10, 1.6, 'D' ], [ 40, 6.4, 'D' ],
[ 0, 0, 'D' ] [ 30, 4.8, 'D' ],
], [ 20, 3.2, 'D' ],
[ 10, 1.6, 'D' ],
// TODO: Fill in multipliers for Chunithm and Ongeki [ 0, 0, 'D' ]
'chu3': [ ],
[ 100.75, 0, 'SSS' ],
[ 100.0, 0, 'SS' ], // TODO: Fill in multipliers for Chunithm and Ongeki
[ 97.5, 0, 'S' ], 'chu3': [
[ 95.0, 0, 'AAA' ], [ 100.9, 215, 'SSS+' ],
[ 92.5, 0, 'AA' ], [ 100.75, 200, 'SSS' ],
[ 90.0, 0, 'A' ], [ 100.0, 0, 'SS' ],
[ 80.0, 0, 'BBB' ], [ 97.5, 0, 'S' ],
[ 70.0, 0, 'BB' ], [ 95.0, 0, 'AAA' ],
[ 60.0, 0, 'B' ], [ 92.5, 0, 'AA' ],
[ 50.0, 0, 'C' ], [ 90.0, 0, 'A' ],
[ 0.0, 0, 'D' ] [ 80.0, 0, 'BBB' ],
], [ 70.0, 0, 'BB' ],
[ 60.0, 0, 'B' ],
'ongeki': [ [ 50.0, 0, 'C' ],
[ 100.75, 0, 'SSS+' ], [ 0.0, 0, 'D' ]
[ 100.0, 0, 'SSS' ], ],
[ 99.0, 0, 'SS' ],
[ 97.0, 0, 'S' ], 'ongeki': [
[ 94.0, 0, 'AAA' ], [ 100.75, 0, 'SSS+' ],
[ 90.0, 0, 'AA' ], [ 100.0, 0, 'SSS' ],
[ 85.0, 0, 'A' ], [ 99.0, 0, 'SS' ],
[ 80.0, 0, 'BBB' ], [ 97.0, 0, 'S' ],
[ 75.0, 0, 'BB' ], [ 94.0, 0, 'AAA' ],
[ 70.0, 0, 'B' ], [ 90.0, 0, 'AA' ],
[ 50.0, 0, 'C' ], [ 85.0, 0, 'A' ],
[ 0.0, 0, 'D' ] [ 80.0, 0, 'BBB' ],
], [ 75.0, 0, 'BB' ],
[ 70.0, 0, 'B' ],
'wacca': [ [ 50.0, 0, 'C' ],
[ 100.0, 0, 'AP' ], [ 0.0, 0, 'D' ]
[ 98.0, 0, 'SSS' ], ],
[ 95.0, 0, 'SS' ],
[ 90.0, 0, 'S' ], 'wacca': [
[ 85.0, 0, 'AAA' ], [ 100.0, 0, 'AP' ],
[ 80.0, 0, 'AA' ], [ 98.0, 0, 'SSS' ],
[ 70.0, 0, 'A' ], [ 95.0, 0, 'SS' ],
[ 60.0, 0, 'B' ], [ 90.0, 0, 'S' ],
[ 1.0, 0, 'C' ], [ 85.0, 0, 'AAA' ],
[ 0.0, 0, 'D' ] [ 80.0, 0, 'AA' ],
] [ 70.0, 0, 'A' ],
} [ 60.0, 0, 'B' ],
[ 1.0, 0, 'C' ],
export function getMult(achievement: number, game: GameName) { [ 0.0, 0, 'D' ]
achievement /= 10000 ]
const mt = multTable[game] }
for (let i = 0; i < mt.length; i++) {
if (achievement >= (mt[i][0] as number)) return mt[i] export function getMult(achievement: number, game: GameName) {
} achievement /= 10000
return [ 0, 0, 0 ] const mt = multTable[game]
} for (let i = 0; i < mt.length; i++) {
if (achievement >= (mt[i][0] as number)) return mt[i]
export function roundFloor(achievement: number, game: GameName, digits = 2) { }
// Round, but if the rounded number reaches the next rank, use floor instead return [ 0, 0, 0 ]
const mult = getMult(achievement, game); }
achievement /= 10000
const rounded = achievement.toFixed(digits); export function roundFloor(achievement: number, game: GameName, digits = 2) {
if (getMult(+rounded * 10000, game)[2] === mult[2] && rounded !== '101.0') return rounded; // Round, but if the rounded number reaches the next rank, use floor instead
return (+rounded - Math.pow(10, -digits)).toFixed(digits); const mult = getMult(achievement, game);
} achievement /= 10000
const rounded = achievement.toFixed(digits);
if (getMult(+rounded * 10000, game)[2] === mult[2] && rounded !== '101.0') return rounded;
return (+rounded - Math.pow(10, -digits)).toFixed(digits);
}
export function chusanRating(lv: number, score: number) {
console.log(lv)
lv = lv * 100
if (score >= 1009000) return lv + 215; // SSS+
if (score >= 1007500) return lv + 200 + (score - 1007500) / 100; // SSS
if (score >= 1005000) return lv + 150 + (score - 1005000) / 50; // SS+
if (score >= 1000000) return lv + 100 + (score - 1000000) / 100; // SS
if (score >= 975000) return lv + (score - 975000) / 250; // S+, S
if (score >= 925000) return lv - 300 + (score - 925000) * 3 / 500; // AA
if (score >= 900000) return lv - 500 + (score - 900000) * 4 / 500; // A
if (score >= 800000) return ((lv - 500) / 2 + (score - 800000) * ((lv - 500) / 2) / (100000)); // BBB
return 0; // C
}
interface ParsedComposition {
musicId: number
diffId: number // ID of the difficulty
score: number
cutoff: number
mult: number
rank: string // e.g. 'SSS+'
difficulty?: number // Actual difficulty of the map
img: string
ratingChange?: number // Rating change after playing this map
}
export function parseComposition(item: string, meta: MusicMeta, game: GameName): ParsedComposition {
// Chuni & ongeki: musicId, difficultId, score
// Mai: musicId, level (difficultyId), romVersion, achievement (score)
const mapData = item.split(':').map(Number)
if (game === 'mai2') mapData.splice(2, 1)
const [ musicId, diffId, score ] = mapData
// Get score multiplier
const tup = getMult(score, game)
const [ cutoff, mult ] = [ +tup[0], +tup[1] ]
const rank = tup[2] as string
let diff = meta?.notes?.[mapData[1] === 10 ? 0 : mapData[1]]?.lv
function calcDxChange() {
if (!diff) return
if (game === 'mai2')
return Math.floor(diff * +mult * (Math.min(100.5, mapData[3] / 10000) / 100))
if (game === 'chu3')
return chusanRating(diff, score) / 100
}
return {
musicId,
diffId,
score,
cutoff,
mult,
rank,
difficulty: diff,
img: `${DATA_HOST}/d/${game}/music/00${mapData[0].toString().padStart(6, '0').substring(2)}.png`,
ratingChange: calcDxChange()
}
}

View File

@@ -260,6 +260,8 @@
<RatingComposition title="B35" comp={d.user.ratingComposition.best35} {allMusics} {game}/> <RatingComposition title="B35" comp={d.user.ratingComposition.best35} {allMusics} {game}/>
<RatingComposition title="B15" comp={d.user.ratingComposition.best15} {allMusics} {game}/> <RatingComposition title="B15" comp={d.user.ratingComposition.best15} {allMusics} {game}/>
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} {game}/> <RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} {game}/>
<RatingComposition title="N10" comp={d.user.ratingComposition.next10} {allMusics} {game}/>
<RatingComposition title="Recent 40" comp={d.user.ratingComposition.recent10} {allMusics} {game}/>
<div class="recent"> <div class="recent">
<h2>{t('UserHome.RecentScores')}</h2> <h2>{t('UserHome.RecentScores')}</h2>