Refactored user scores
This commit is contained in:
parent
825ac523c2
commit
f3d85b47b0
@ -112,64 +112,11 @@ data class ReplayPairViewerData(
|
|||||||
val judgements2: List<CircleguardService.ScoreJudgement>
|
val judgements2: List<CircleguardService.ScoreJudgement>
|
||||||
)
|
)
|
||||||
|
|
||||||
data class UserReplayData(
|
|
||||||
val replay_id: UUID,
|
|
||||||
val osu_replay_id: Long?,
|
|
||||||
val username: String,
|
|
||||||
val beatmap_id: Int,
|
|
||||||
val beatmap_beatmapset_id: Int,
|
|
||||||
val beatmap_artist: String,
|
|
||||||
val beatmap_title: String,
|
|
||||||
val beatmap_star_rating: Double,
|
|
||||||
val beatmap_creator: String,
|
|
||||||
val beatmap_version: String,
|
|
||||||
val score: Int,
|
|
||||||
val mods: List<String>,
|
|
||||||
val ur: Double?,
|
|
||||||
val adjusted_ur: Double?,
|
|
||||||
val frametime: Int,
|
|
||||||
val snaps: Int,
|
|
||||||
val hits: Int,
|
|
||||||
|
|
||||||
var mean_error: Double?,
|
|
||||||
var error_variance: Double?,
|
|
||||||
var error_standard_deviation: Double?,
|
|
||||||
var minimum_error: Double?,
|
|
||||||
var maximum_error: Double?,
|
|
||||||
var error_range: Double?,
|
|
||||||
var error_coefficient_of_variation: Double?,
|
|
||||||
var error_kurtosis: Double?,
|
|
||||||
var error_skewness: Double?,
|
|
||||||
|
|
||||||
var comparable_samples: Int? = null,
|
|
||||||
var comparable_mean_error: Double? = null,
|
|
||||||
var comparable_error_variance: Double? = null,
|
|
||||||
var comparable_error_standard_deviation: Double? = null,
|
|
||||||
var comparable_minimum_error: Double? = null,
|
|
||||||
var comparable_maximum_error: Double? = null,
|
|
||||||
var comparable_error_range: Double? = null,
|
|
||||||
var comparable_error_coefficient_of_variation: Double? = null,
|
|
||||||
var comparable_error_kurtosis: Double? = null,
|
|
||||||
var comparable_error_skewness: Double? = null,
|
|
||||||
|
|
||||||
val perfect: Boolean,
|
|
||||||
val max_combo: Int,
|
|
||||||
|
|
||||||
val count_300: Int,
|
|
||||||
val count_100: Int,
|
|
||||||
val count_50: Int,
|
|
||||||
val count_miss: Int,
|
|
||||||
|
|
||||||
val similar_scores: List<ReplayDataSimilarScore>,
|
|
||||||
val error_distribution: Map<Int, DistributionEntry>,
|
|
||||||
val charts: List<ReplayDataChart>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ReplayData(
|
data class ReplayData(
|
||||||
val replay_id: Long,
|
val replay_id: Long,
|
||||||
val user_id: Int,
|
val user_id: Int?,
|
||||||
val username: String,
|
val username: String,
|
||||||
val date: String,
|
val date: String?,
|
||||||
val beatmap_id: Int,
|
val beatmap_id: Int,
|
||||||
val beatmap_beatmapset_id: Int,
|
val beatmap_beatmapset_id: Int,
|
||||||
val beatmap_artist: String,
|
val beatmap_artist: String,
|
||||||
@ -189,10 +136,9 @@ data class ReplayData(
|
|||||||
val beatmap_count_spinners: Int?,
|
val beatmap_count_spinners: Int?,
|
||||||
val score: Int,
|
val score: Int,
|
||||||
val mods: List<String>,
|
val mods: List<String>,
|
||||||
val rank: String,
|
val rank: String?,
|
||||||
val ur: Double?,
|
val ur: Double?,
|
||||||
val adjusted_ur: Double?,
|
val adjusted_ur: Double?,
|
||||||
val average_ur: Double?,
|
|
||||||
val frametime: Int,
|
val frametime: Int,
|
||||||
val snaps: Int,
|
val snaps: Int,
|
||||||
val hits: Int,
|
val hits: Int,
|
||||||
@ -217,8 +163,9 @@ data class ReplayData(
|
|||||||
var comparable_error_coefficient_of_variation: Double? = null,
|
var comparable_error_coefficient_of_variation: Double? = null,
|
||||||
var comparable_error_kurtosis: Double? = null,
|
var comparable_error_kurtosis: Double? = null,
|
||||||
var comparable_error_skewness: Double? = null,
|
var comparable_error_skewness: Double? = null,
|
||||||
|
var comparable_adjusted_ur: Double? = null,
|
||||||
|
|
||||||
val pp: Double,
|
val pp: Double?,
|
||||||
val perfect: Boolean,
|
val perfect: Boolean,
|
||||||
val max_combo: Int,
|
val max_combo: Int,
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,7 @@ class ScoreController(
|
|||||||
|
|
||||||
// Sort replays by date (the first replay will always be the oldest)
|
// Sort replays by date (the first replay will always be the oldest)
|
||||||
val replays = listOf(replay1Data, replay2Data)
|
val replays = listOf(replay1Data, replay2Data)
|
||||||
.sortedBy { Format.parseStringToDate(it.date) }
|
.sortedBy { Format.parseStringToDate(it.date!!) }
|
||||||
|
|
||||||
val replayPair = ReplayPair(replays, replayPairStatistics)
|
val replayPair = ReplayPair(replays, replayPairStatistics)
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
package com.nisemoe.nise.controller
|
package com.nisemoe.nise.controller
|
||||||
|
|
||||||
import com.nisemoe.nise.UserReplayData
|
import com.nisemoe.nise.ReplayData
|
||||||
import com.nisemoe.nise.database.UserScoreService
|
import com.nisemoe.nise.database.UserScoreService
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class UserScoresController(
|
class UserScoresController(
|
||||||
@ -14,7 +14,7 @@ class UserScoresController(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping("user-score/{replayId}")
|
@GetMapping("user-score/{replayId}")
|
||||||
fun getScoreDetails(@PathVariable replayId: UUID): ResponseEntity<UserReplayData> {
|
fun getScoreDetails(@PathVariable replayId: UUID): ResponseEntity<ReplayData> {
|
||||||
val replayData = this.userScoreService.getReplayData(replayId)
|
val replayData = this.userScoreService.getReplayData(replayId)
|
||||||
?: return ResponseEntity.notFound().build()
|
?: return ResponseEntity.notFound().build()
|
||||||
|
|
||||||
|
|||||||
@ -44,23 +44,4 @@ class BeatmapService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAverageUR(beatmapId: Int, excludeReplayId: Long): Double? {
|
|
||||||
val condition = SCORES.BEATMAP_ID.eq(beatmapId)
|
|
||||||
.and(SCORES.UR.isNotNull)
|
|
||||||
.and(SCORES.REPLAY_ID.notEqual(excludeReplayId))
|
|
||||||
|
|
||||||
val totalScores = dslContext.fetchCount(
|
|
||||||
SCORES, condition
|
|
||||||
)
|
|
||||||
|
|
||||||
if(totalScores < 50)
|
|
||||||
return null
|
|
||||||
|
|
||||||
return dslContext
|
|
||||||
.select(avg(SCORES.UR))
|
|
||||||
.from(SCORES)
|
|
||||||
.where(condition)
|
|
||||||
.fetchOneInto(Double::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -174,7 +174,6 @@ class ScoreService(
|
|||||||
.fetchOne() ?: return null
|
.fetchOne() ?: return null
|
||||||
|
|
||||||
val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
|
val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
|
||||||
val averageUR = beatmapService.getAverageUR(beatmapId = beatmapId, excludeReplayId = replayId)
|
|
||||||
val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java))
|
val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java))
|
||||||
val charts = this.getCharts(result)
|
val charts = this.getCharts(result)
|
||||||
|
|
||||||
@ -207,7 +206,6 @@ class ScoreService(
|
|||||||
score = result.get(SCORES.SCORE, Int::class.java),
|
score = result.get(SCORES.SCORE, Int::class.java),
|
||||||
mods = Mod.parseModCombination(result.get(SCORES.MODS, Int::class.java)),
|
mods = Mod.parseModCombination(result.get(SCORES.MODS, Int::class.java)),
|
||||||
rank = result.get(SCORES.RANK, String::class.java),
|
rank = result.get(SCORES.RANK, String::class.java),
|
||||||
average_ur = averageUR,
|
|
||||||
snaps = result.get(SCORES.SNAPS, Int::class.java),
|
snaps = result.get(SCORES.SNAPS, Int::class.java),
|
||||||
hits = result.get(SCORES.EDGE_HITS, Int::class.java),
|
hits = result.get(SCORES.EDGE_HITS, Int::class.java),
|
||||||
perfect = result.get(SCORES.PERFECT, Boolean::class.java),
|
perfect = result.get(SCORES.PERFECT, Boolean::class.java),
|
||||||
@ -482,7 +480,8 @@ class ScoreService(
|
|||||||
avg(SCORES.ERROR_RANGE).`as`("avg_error_range"),
|
avg(SCORES.ERROR_RANGE).`as`("avg_error_range"),
|
||||||
avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"),
|
avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"),
|
||||||
avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"),
|
avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"),
|
||||||
avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness")
|
avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness"),
|
||||||
|
avg(SCORES.ADJUSTED_UR).`as`("avg_adjusted_ur")
|
||||||
)
|
)
|
||||||
.from(SCORES)
|
.from(SCORES)
|
||||||
.where(SCORES.BEATMAP_ID.eq(replayData.beatmap_id))
|
.where(SCORES.BEATMAP_ID.eq(replayData.beatmap_id))
|
||||||
@ -500,6 +499,7 @@ class ScoreService(
|
|||||||
replayData.comparable_error_coefficient_of_variation = otherScores.get("avg_error_coefficient_of_variation", Double::class.java)
|
replayData.comparable_error_coefficient_of_variation = otherScores.get("avg_error_coefficient_of_variation", Double::class.java)
|
||||||
replayData.comparable_error_kurtosis = otherScores.get("avg_error_kurtosis", Double::class.java)
|
replayData.comparable_error_kurtosis = otherScores.get("avg_error_kurtosis", Double::class.java)
|
||||||
replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java)
|
replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java)
|
||||||
|
replayData.comparable_adjusted_ur = otherScores.get("avg_adjusted_ur", Double::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapLegacyJudgement(judgementType: JudgementType): CircleguardService.JudgementType {
|
fun mapLegacyJudgement(judgementType: JudgementType): CircleguardService.JudgementType {
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
package com.nisemoe.nise.database
|
package com.nisemoe.nise.database
|
||||||
|
|
||||||
import com.nisemoe.generated.tables.references.*
|
import com.nisemoe.generated.tables.references.*
|
||||||
import com.nisemoe.nise.*
|
import com.nisemoe.nise.DistributionEntry
|
||||||
|
import com.nisemoe.nise.Format
|
||||||
|
import com.nisemoe.nise.ReplayData
|
||||||
|
import com.nisemoe.nise.ReplayDataSimilarScore
|
||||||
import com.nisemoe.nise.osu.Mod
|
import com.nisemoe.nise.osu.Mod
|
||||||
import com.nisemoe.nise.service.AuthService
|
|
||||||
import com.nisemoe.nise.service.CompressJudgements
|
import com.nisemoe.nise.service.CompressJudgements
|
||||||
import org.jooq.DSLContext
|
import org.jooq.DSLContext
|
||||||
import org.jooq.Record
|
|
||||||
import org.jooq.impl.DSL
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -16,18 +16,11 @@ import kotlin.math.roundToInt
|
|||||||
@Service
|
@Service
|
||||||
class UserScoreService(
|
class UserScoreService(
|
||||||
private val dslContext: DSLContext,
|
private val dslContext: DSLContext,
|
||||||
private val authService: AuthService,
|
private val scoreService: ScoreService,
|
||||||
private val compressJudgements: CompressJudgements
|
private val compressJudgements: CompressJudgements
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun getCharts(db: Record): List<ReplayDataChart> {
|
fun getReplayData(replayId: UUID): ReplayData? {
|
||||||
if (!authService.isAdmin()) return emptyList()
|
|
||||||
|
|
||||||
return listOf(USER_SCORES.SLIDEREND_RELEASE_TIMES to "slider end release times", USER_SCORES.KEYPRESSES_TIMES to "keypress times")
|
|
||||||
.mapNotNull { (scoreType, title) -> db.get(scoreType)?.let { ReplayDataChart(title, it.filterNotNull()) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getReplayData(replayId: UUID): UserReplayData? {
|
|
||||||
val result = dslContext.select(
|
val result = dslContext.select(
|
||||||
USER_SCORES.ID,
|
USER_SCORES.ID,
|
||||||
USER_SCORES.PLAYER_NAME,
|
USER_SCORES.PLAYER_NAME,
|
||||||
@ -38,6 +31,16 @@ class UserScoreService(
|
|||||||
BEATMAPS.STAR_RATING,
|
BEATMAPS.STAR_RATING,
|
||||||
BEATMAPS.CREATOR,
|
BEATMAPS.CREATOR,
|
||||||
BEATMAPS.VERSION,
|
BEATMAPS.VERSION,
|
||||||
|
BEATMAPS.TOTAL_LENGTH,
|
||||||
|
BEATMAPS.MAX_COMBO,
|
||||||
|
BEATMAPS.BPM,
|
||||||
|
BEATMAPS.ACCURACY,
|
||||||
|
BEATMAPS.AR,
|
||||||
|
BEATMAPS.CS,
|
||||||
|
BEATMAPS.DRAIN,
|
||||||
|
BEATMAPS.COUNT_CIRCLES,
|
||||||
|
BEATMAPS.COUNT_SPINNERS,
|
||||||
|
BEATMAPS.COUNT_SLIDERS,
|
||||||
USER_SCORES.ONLINE_SCORE_ID,
|
USER_SCORES.ONLINE_SCORE_ID,
|
||||||
USER_SCORES.TOTAL_SCORE,
|
USER_SCORES.TOTAL_SCORE,
|
||||||
USER_SCORES.FRAMETIME,
|
USER_SCORES.FRAMETIME,
|
||||||
@ -72,11 +75,10 @@ class UserScoreService(
|
|||||||
|
|
||||||
val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
|
val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
|
||||||
val hitDistribution = this.getHitDistribution(result.get(USER_SCORES.JUDGEMENTS, ByteArray::class.java))
|
val hitDistribution = this.getHitDistribution(result.get(USER_SCORES.JUDGEMENTS, ByteArray::class.java))
|
||||||
val charts = this.getCharts(result)
|
val charts = this.scoreService.getCharts(result)
|
||||||
|
|
||||||
val replayData = UserReplayData(
|
val replayData = ReplayData(
|
||||||
replay_id = replayId,
|
replay_id = result.get(USER_SCORES.ONLINE_SCORE_ID, Long::class.java),
|
||||||
osu_replay_id = result.get(USER_SCORES.ONLINE_SCORE_ID, Long::class.java),
|
|
||||||
username = result.get(USER_SCORES.PLAYER_NAME, String::class.java),
|
username = result.get(USER_SCORES.PLAYER_NAME, String::class.java),
|
||||||
beatmap_id = beatmapId,
|
beatmap_id = beatmapId,
|
||||||
beatmap_beatmapset_id = result.get(BEATMAPS.BEATMAPSET_ID, Int::class.java),
|
beatmap_beatmapset_id = result.get(BEATMAPS.BEATMAPSET_ID, Int::class.java),
|
||||||
@ -85,6 +87,16 @@ class UserScoreService(
|
|||||||
beatmap_star_rating = result.get(BEATMAPS.STAR_RATING, Double::class.java),
|
beatmap_star_rating = result.get(BEATMAPS.STAR_RATING, Double::class.java),
|
||||||
beatmap_creator = result.get(BEATMAPS.CREATOR, String::class.java),
|
beatmap_creator = result.get(BEATMAPS.CREATOR, String::class.java),
|
||||||
beatmap_version = result.get(BEATMAPS.VERSION, String::class.java),
|
beatmap_version = result.get(BEATMAPS.VERSION, String::class.java),
|
||||||
|
beatmap_bpm = result.get(BEATMAPS.BPM, Double::class.java),
|
||||||
|
beatmap_max_combo = result.get(BEATMAPS.MAX_COMBO, Int::class.java),
|
||||||
|
beatmap_total_length = result.get(BEATMAPS.TOTAL_LENGTH, Int::class.java),
|
||||||
|
beatmap_accuracy = result.get(BEATMAPS.ACCURACY, Double::class.java),
|
||||||
|
beatmap_ar = result.get(BEATMAPS.AR, Double::class.java),
|
||||||
|
beatmap_cs = result.get(BEATMAPS.CS, Double::class.java),
|
||||||
|
beatmap_drain = result.get(BEATMAPS.DRAIN, Double::class.java),
|
||||||
|
beatmap_count_circles = result.get(BEATMAPS.COUNT_CIRCLES, Int::class.java),
|
||||||
|
beatmap_count_spinners = result.get(BEATMAPS.COUNT_SPINNERS, Int::class.java),
|
||||||
|
beatmap_count_sliders = result.get(BEATMAPS.COUNT_SLIDERS, Int::class.java),
|
||||||
frametime = result.get(USER_SCORES.FRAMETIME, Double::class.java).toInt(),
|
frametime = result.get(USER_SCORES.FRAMETIME, Double::class.java).toInt(),
|
||||||
ur = result.get(USER_SCORES.UR, Double::class.java),
|
ur = result.get(USER_SCORES.UR, Double::class.java),
|
||||||
adjusted_ur = result.get(USER_SCORES.ADJUSTED_UR, Double::class.java),
|
adjusted_ur = result.get(USER_SCORES.ADJUSTED_UR, Double::class.java),
|
||||||
@ -109,9 +121,13 @@ class UserScoreService(
|
|||||||
error_kurtosis = result.get(USER_SCORES.ERROR_KURTOSIS, Double::class.java),
|
error_kurtosis = result.get(USER_SCORES.ERROR_KURTOSIS, Double::class.java),
|
||||||
error_skewness = result.get(USER_SCORES.ERROR_SKEWNESS, Double::class.java),
|
error_skewness = result.get(USER_SCORES.ERROR_SKEWNESS, Double::class.java),
|
||||||
charts = charts,
|
charts = charts,
|
||||||
similar_scores = this.getSimilarScores(replayId)
|
similar_scores = this.getSimilarScores(replayId),
|
||||||
|
date = null,
|
||||||
|
pp = null,
|
||||||
|
rank = null,
|
||||||
|
user_id = null
|
||||||
)
|
)
|
||||||
this.loadComparableReplayData(replayData)
|
this.scoreService.loadComparableReplayData(replayData)
|
||||||
return replayData
|
return replayData
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,46 +164,6 @@ class UserScoreService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadComparableReplayData(replayData: UserReplayData) {
|
|
||||||
// Total samples
|
|
||||||
val totalSamples = dslContext.fetchCount(
|
|
||||||
SCORES, SCORES.BEATMAP_ID.eq(replayData.beatmap_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
if(totalSamples <= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We will select same beatmap_id and same mods
|
|
||||||
val otherScores = dslContext.select(
|
|
||||||
DSL.avg(SCORES.MEAN_ERROR).`as`("avg_mean_error"),
|
|
||||||
DSL.avg(SCORES.ERROR_VARIANCE).`as`("avg_error_variance"),
|
|
||||||
DSL.avg(SCORES.ERROR_STANDARD_DEVIATION).`as`("avg_error_standard_deviation"),
|
|
||||||
DSL.avg(SCORES.MINIMUM_ERROR).`as`("avg_minimum_error"),
|
|
||||||
DSL.avg(SCORES.MAXIMUM_ERROR).`as`("avg_maximum_error"),
|
|
||||||
DSL.avg(SCORES.ERROR_RANGE).`as`("avg_error_range"),
|
|
||||||
DSL.avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"),
|
|
||||||
DSL.avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"),
|
|
||||||
DSL.avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness")
|
|
||||||
)
|
|
||||||
.from(SCORES)
|
|
||||||
.where(SCORES.BEATMAP_ID.eq(replayData.beatmap_id))
|
|
||||||
.fetchOne() ?: return
|
|
||||||
|
|
||||||
replayData.comparable_samples = totalSamples
|
|
||||||
|
|
||||||
replayData.comparable_mean_error = otherScores.get("avg_mean_error", Double::class.java)
|
|
||||||
replayData.comparable_error_variance = otherScores.get("avg_error_variance", Double::class.java)
|
|
||||||
replayData.comparable_error_standard_deviation = otherScores.get("avg_error_standard_deviation", Double::class.java)
|
|
||||||
replayData.comparable_minimum_error = otherScores.get("avg_minimum_error", Double::class.java)
|
|
||||||
replayData.comparable_maximum_error = otherScores.get("avg_maximum_error", Double::class.java)
|
|
||||||
replayData.comparable_error_range = otherScores.get("avg_error_range", Double::class.java)
|
|
||||||
replayData.comparable_error_coefficient_of_variation = otherScores.get("avg_error_coefficient_of_variation", Double::class.java)
|
|
||||||
replayData.comparable_error_kurtosis = otherScores.get("avg_error_kurtosis", Double::class.java)
|
|
||||||
replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun getHitDistribution(compressedJudgements: ByteArray): Map<Int, DistributionEntry> {
|
fun getHitDistribution(compressedJudgements: ByteArray): Map<Int, DistributionEntry> {
|
||||||
val judgements = compressJudgements.deserialize(compressedJudgements)
|
val judgements = compressJudgements.deserialize(compressedJudgements)
|
||||||
|
|
||||||
|
|||||||
@ -194,8 +194,8 @@ class SendScoresToDiscord(
|
|||||||
)
|
)
|
||||||
|
|
||||||
statisticsEmbed.addEmbed(name = "Played by", value = "[${replayData.username}](https://osu.ppy.sh/users/${replayData.user_id})")
|
statisticsEmbed.addEmbed(name = "Played by", value = "[${replayData.username}](https://osu.ppy.sh/users/${replayData.user_id})")
|
||||||
statisticsEmbed.addEmbed(name = "Played at", value = replayData.date)
|
statisticsEmbed.addEmbed(name = "Played at", value = replayData.date!!)
|
||||||
statisticsEmbed.addEmbed(name = "PP", value = replayData.pp.roundToInt().toString())
|
statisticsEmbed.addEmbed(name = "PP", value = replayData.pp?.roundToInt().toString())
|
||||||
if(replayData.mods.isNotEmpty())
|
if(replayData.mods.isNotEmpty())
|
||||||
statisticsEmbed.addEmbed(name = "Mods", value = replayData.mods.joinToString(""))
|
statisticsEmbed.addEmbed(name = "Mods", value = replayData.mods.joinToString(""))
|
||||||
statisticsEmbed.addEmbed(name = "Max Combo", value = "${replayData.max_combo}x")
|
statisticsEmbed.addEmbed(name = "Max Combo", value = "${replayData.max_combo}x")
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {ViewScoreComponent} from "./view-score/view-score.component";
|
|||||||
import {ViewUserComponent} from "./view-user/view-user.component";
|
import {ViewUserComponent} from "./view-user/view-user.component";
|
||||||
import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component";
|
import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component";
|
||||||
import {SearchComponent} from "./search/search.component";
|
import {SearchComponent} from "./search/search.component";
|
||||||
import {ViewUserScoreComponent} from "./view-user-score/view-user-score.component";
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
|
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
|
||||||
@ -18,10 +17,10 @@ const routes: Routes = [
|
|||||||
|
|
||||||
{path: 'u/:userId', component: ViewUserComponent},
|
{path: 'u/:userId', component: ViewUserComponent},
|
||||||
{path: 's/:replayId', component: ViewScoreComponent},
|
{path: 's/:replayId', component: ViewScoreComponent},
|
||||||
|
{path: 'c/:userReplayId', component: ViewScoreComponent},
|
||||||
{path: 'search', component: SearchComponent},
|
{path: 'search', component: SearchComponent},
|
||||||
{path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent},
|
|
||||||
|
|
||||||
{path: 'c/:replayId', component: ViewUserScoreComponent},
|
{path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent},
|
||||||
|
|
||||||
{path: '**', component: HomeComponent, title: '/nise.moe/'},
|
{path: '**', component: HomeComponent, title: '/nise.moe/'},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {ReplayData, UserReplayData} from "./replays";
|
import {ReplayData} from "./replays";
|
||||||
|
|
||||||
export function formatDuration(seconds: number): string | null {
|
export function formatDuration(seconds: number): string | null {
|
||||||
if(!seconds) {
|
if(!seconds) {
|
||||||
@ -31,7 +31,7 @@ export function countryCodeToFlag(countryCode: string): string {
|
|||||||
return String.fromCodePoint(...codePoints);
|
return String.fromCodePoint(...codePoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateAccuracy(replayData: ReplayData | UserReplayData): number {
|
export function calculateAccuracy(replayData: ReplayData): number {
|
||||||
if(!replayData) {
|
if(!replayData) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,59 +13,6 @@ export interface ReplayDataSimilarScore {
|
|||||||
correlation: number;
|
correlation: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserReplayData {
|
|
||||||
replay_id: string;
|
|
||||||
osu_replay_id?: string;
|
|
||||||
username: string;
|
|
||||||
beatmap_id: number;
|
|
||||||
beatmap_beatmapset_id: number;
|
|
||||||
beatmap_artist: string;
|
|
||||||
beatmap_title: string;
|
|
||||||
beatmap_star_rating: number;
|
|
||||||
beatmap_creator: string;
|
|
||||||
beatmap_version: string;
|
|
||||||
score: number;
|
|
||||||
mods: string[];
|
|
||||||
ur?: number;
|
|
||||||
adjusted_ur?: number;
|
|
||||||
frametime: number;
|
|
||||||
snaps: number;
|
|
||||||
hits: number;
|
|
||||||
|
|
||||||
mean_error?: number;
|
|
||||||
error_variance?: number;
|
|
||||||
error_standard_deviation?: number;
|
|
||||||
minimum_error?: number;
|
|
||||||
maximum_error?: number;
|
|
||||||
error_range?: number;
|
|
||||||
error_coefficient_of_variation?: number;
|
|
||||||
error_kurtosis?: number;
|
|
||||||
error_skewness?: number;
|
|
||||||
|
|
||||||
perfect: boolean;
|
|
||||||
max_combo: number;
|
|
||||||
|
|
||||||
count_300: number;
|
|
||||||
count_100: number;
|
|
||||||
count_50: number;
|
|
||||||
count_miss: number;
|
|
||||||
|
|
||||||
comparable_samples?: number;
|
|
||||||
comparable_mean_error?: number,
|
|
||||||
comparable_error_variance?: number,
|
|
||||||
comparable_error_standard_deviation?: number,
|
|
||||||
comparable_minimum_error?: number,
|
|
||||||
comparable_maximum_error?: number,
|
|
||||||
comparable_error_range?: number,
|
|
||||||
comparable_error_coefficient_of_variation?: number,
|
|
||||||
comparable_error_kurtosis?: number,
|
|
||||||
comparable_error_skewness?: number,
|
|
||||||
|
|
||||||
similar_scores: ReplayDataSimilarScore[];
|
|
||||||
error_distribution: ErrorDistribution;
|
|
||||||
charts: ReplayDataChart[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMockReplayData(): ReplayData {
|
export function getMockReplayData(): ReplayData {
|
||||||
return {
|
return {
|
||||||
'replay_id': 123,
|
'replay_id': 123,
|
||||||
@ -93,7 +40,6 @@ export function getMockReplayData(): ReplayData {
|
|||||||
'mods': ['mods'],
|
'mods': ['mods'],
|
||||||
'rank': 'rank',
|
'rank': 'rank',
|
||||||
'ur': 123,
|
'ur': 123,
|
||||||
'average_ur': 123,
|
|
||||||
'adjusted_ur': 123,
|
'adjusted_ur': 123,
|
||||||
'frametime': 123,
|
'frametime': 123,
|
||||||
'snaps': 123,
|
'snaps': 123,
|
||||||
@ -146,7 +92,6 @@ export interface ReplayData {
|
|||||||
mods: string[];
|
mods: string[];
|
||||||
rank: string;
|
rank: string;
|
||||||
ur: number;
|
ur: number;
|
||||||
average_ur: number | null;
|
|
||||||
adjusted_ur?: number;
|
adjusted_ur?: number;
|
||||||
frametime: number;
|
frametime: number;
|
||||||
snaps: number;
|
snaps: number;
|
||||||
@ -176,6 +121,7 @@ export interface ReplayData {
|
|||||||
comparable_error_coefficient_of_variation?: number,
|
comparable_error_coefficient_of_variation?: number,
|
||||||
comparable_error_kurtosis?: number,
|
comparable_error_kurtosis?: number,
|
||||||
comparable_error_skewness?: number,
|
comparable_error_skewness?: number,
|
||||||
|
comparable_adjusted_ur?: number;
|
||||||
|
|
||||||
count_300: number,
|
count_300: number,
|
||||||
count_100: number,
|
count_100: number,
|
||||||
|
|||||||
@ -33,9 +33,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-player__row score-player__row--player mt-2">
|
<div class="score-player__row score-player__row--player mt-2">
|
||||||
Played by <a [routerLink]="['/u/' + this.replayData.username]">{{ this.replayData.username }}</a> <a class="btn" style="margin-left: 5px" href="https://osu.ppy.sh/users/{{ this.replayData.user_id }}" target="_blank">osu!web</a>
|
Played by <a [routerLink]="['/u/' + this.replayData.username]">{{ this.replayData.username }}</a> <a *ngIf="this.replayData.user_id" class="btn" style="margin-left: 5px" href="https://osu.ppy.sh/users/{{ this.replayData.user_id }}" target="_blank">osu!web</a>
|
||||||
<br>
|
<ng-container *ngIf="!this.isUserScore">
|
||||||
Submitted on <strong>{{ this.replayData.date }}</strong>
|
<br>
|
||||||
|
Submitted on <strong>{{ this.replayData.date }}</strong>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -130,7 +132,7 @@
|
|||||||
<span class="stat-label">Max Combo</span>
|
<span class="stat-label">Max Combo</span>
|
||||||
<span class="stat-value">{{ this.replayData.max_combo }}x <span *ngIf="this.replayData.perfect" class="badge badge-green">perfect</span></span>
|
<span class="stat-value">{{ this.replayData.max_combo }}x <span *ngIf="this.replayData.perfect" class="badge badge-green">perfect</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat" *ngIf="this.replayData.pp">
|
||||||
<span class="stat-label">PP</span>
|
<span class="stat-label">PP</span>
|
||||||
<span class="stat-value">{{ this.replayData.pp | number: '1.0-0' }}</span>
|
<span class="stat-value">{{ this.replayData.pp | number: '1.0-0' }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -218,10 +220,6 @@
|
|||||||
|
|
||||||
<div class="main term mb-2" *ngIf="this.replayData.mean_error">
|
<div class="main term mb-2" *ngIf="this.replayData.mean_error">
|
||||||
<h1># nerd stats</h1>
|
<h1># nerd stats</h1>
|
||||||
<div class="alert text-center mb-2" *ngIf="this.replayData.average_ur">
|
|
||||||
<p class="bold">Heads up!</p>
|
|
||||||
<p>The average cvUR for this beatmap is <span class="bold">{{ this.replayData.average_ur | number: '1.0-2' }}</span></p>
|
|
||||||
</div>
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<th></th>
|
<th></th>
|
||||||
@ -269,6 +267,11 @@
|
|||||||
<td class="text-center">{{ this.replayData.error_skewness | number: '1.2-2'}}</td>
|
<td class="text-center">{{ this.replayData.error_skewness | number: '1.2-2'}}</td>
|
||||||
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_skewness | number: '1.2-2' }}</td>
|
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_skewness | number: '1.2-2' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Adjusted cvUR</td>
|
||||||
|
<td class="text-center">{{ this.replayData.adjusted_ur | number: '1.2-2'}}</td>
|
||||||
|
<td *ngIf="this.replayData.comparable_adjusted_ur" class="text-center">{{ this.replayData.comparable_adjusted_ur | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -276,7 +279,7 @@
|
|||||||
<app-chart [title]="chart.title" [data]="chart.data"></app-chart>
|
<app-chart [title]="chart.title" [data]="chart.data"></app-chart>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="hasReplay()">
|
<ng-container *ngIf="hasErrorDistribution()">
|
||||||
<app-chart-hit-distribution [errorDistribution]="replayData!!.error_distribution" [mods]="replayData!!.mods"></app-chart-hit-distribution>
|
<app-chart-hit-distribution [errorDistribution]="replayData!!.error_distribution" [mods]="replayData!!.mods"></app-chart-hit-distribution>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
import {Component, OnInit, ViewChild} from '@angular/core';
|
||||||
import {ChartConfiguration} from 'chart.js';
|
|
||||||
import {BaseChartDirective, NgChartsModule} from 'ng2-charts';
|
import {BaseChartDirective, NgChartsModule} from 'ng2-charts';
|
||||||
import {HttpClient} from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
import {environment} from "../../environments/environment";
|
import {environment} from "../../environments/environment";
|
||||||
import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
|
import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
|
||||||
import {ActivatedRoute, RouterLink} from "@angular/router";
|
import {ActivatedRoute, RouterLink} from "@angular/router";
|
||||||
import {catchError, throwError} from "rxjs";
|
import {catchError, throwError} from "rxjs";
|
||||||
import {DistributionEntry, ReplayData} from "../replays";
|
import {ReplayData} from "../replays";
|
||||||
import {calculateAccuracy} from "../format";
|
import {calculateAccuracy} from "../format";
|
||||||
import {Title} from "@angular/platform-browser";
|
import {Title} from "@angular/platform-browser";
|
||||||
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
|
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
|
||||||
@ -43,6 +42,9 @@ export class ViewScoreComponent implements OnInit {
|
|||||||
isLoading = false;
|
isLoading = false;
|
||||||
error: string | null = null;
|
error: string | null = null;
|
||||||
replayData: ReplayData | null = null;
|
replayData: ReplayData | null = null;
|
||||||
|
|
||||||
|
isUserScore: boolean = false;
|
||||||
|
userReplayId: string | null = null;
|
||||||
replayId: number | null = null;
|
replayId: number | null = null;
|
||||||
|
|
||||||
constructor(private http: HttpClient,
|
constructor(private http: HttpClient,
|
||||||
@ -50,15 +52,27 @@ export class ViewScoreComponent implements OnInit {
|
|||||||
private title: Title
|
private title: Title
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
hasReplay(): boolean {
|
hasErrorDistribution(): boolean {
|
||||||
return !!this.replayData?.error_distribution && Object.keys(this.replayData.error_distribution).length > 0;
|
return !!this.replayData?.error_distribution && Object.keys(this.replayData.error_distribution).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasReplay(): boolean {
|
||||||
|
if(this.isUserScore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.hasErrorDistribution();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.activatedRoute.params.subscribe(params => {
|
this.activatedRoute.params.subscribe(params => {
|
||||||
this.replayId = params['replayId'];
|
this.replayId = params['replayId'];
|
||||||
|
this.userReplayId = params['userReplayId'];
|
||||||
if (this.replayId) {
|
if (this.replayId) {
|
||||||
this.loadScoreData();
|
this.loadScoreData(`${environment.apiUrl}/score/${this.replayId}`);
|
||||||
|
}
|
||||||
|
if(this.userReplayId) {
|
||||||
|
this.loadScoreData(`${environment.apiUrl}/user-score/${this.userReplayId}`);
|
||||||
|
this.isUserScore = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -87,9 +101,9 @@ export class ViewScoreComponent implements OnInit {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadScoreData(): void {
|
private loadScoreData(url: string): void {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.http.get<ReplayData>(`${environment.apiUrl}/score/${this.replayId}`).pipe(
|
this.http.get<ReplayData>(url).pipe(
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
return throwError(() => new Error('An error occurred with the request: ' + error.message));
|
return throwError(() => new Error('An error occurred with the request: ' + error.message));
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
/* Flex container */
|
|
||||||
.flex-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px; /* Adjust the gap between items as needed */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Flex items - default to full width to stack on smaller screens */
|
|
||||||
.flex-container > div {
|
|
||||||
flex: 0 0 100%;
|
|
||||||
box-sizing: border-box; /* To include padding and border in the element's total width and height */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive columns */
|
|
||||||
@media (min-width: 768px) { /* Adjust the breakpoint as needed */
|
|
||||||
.flex-container > div {
|
|
||||||
flex: 0 0 15%;
|
|
||||||
max-width: 20%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
<ng-container *ngIf="this.isLoading">
|
|
||||||
<div class="main term">
|
|
||||||
<div class="text-center">
|
|
||||||
Loading, please wait...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="this.error">
|
|
||||||
<div class="main term">
|
|
||||||
<div class="text-center">
|
|
||||||
An error occured. Maybe try again in a bit?
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="this.replayData && !this.isLoading && !this.error">
|
|
||||||
<div class="main term mb-2 text-center">
|
|
||||||
<p>
|
|
||||||
This replay is user-submitted. As such, it might have been edited.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Please take the displayed data with a grain of salt.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="main term mb-2">
|
|
||||||
<div class="fade-stuff">
|
|
||||||
<div class="image-container">
|
|
||||||
<a href="https://osu.ppy.sh/beatmaps/{{ this.replayData.beatmap_id }}?mode=osu" target="_blank">
|
|
||||||
<img ngSrc="https://assets.ppy.sh/beatmaps/{{ this.replayData.beatmap_beatmapset_id }}/covers/cover.jpg" width="260" height="72"
|
|
||||||
alt="Beatmap Cover">
|
|
||||||
<div class="overlay">
|
|
||||||
<h4>
|
|
||||||
{{ this.replayData.beatmap_title }} <span class="text-muted">by</span> {{ this.replayData.beatmap_artist }}
|
|
||||||
</h4>
|
|
||||||
★{{ this.replayData.beatmap_star_rating | number: '1.0-2' }} {{ this.replayData.beatmap_version }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="mt-2 mb-2">
|
|
||||||
|
|
||||||
<div class="badge-list">
|
|
||||||
<span class="badge" *ngFor="let mod of this.replayData.mods">
|
|
||||||
{{ mod }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
{{ this.replayData.score | number }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
|
||||||
<div style="flex: 1; padding-right: 10px;">
|
|
||||||
<ul style="line-height: 2.2">
|
|
||||||
<li>Played by: {{ this.replayData.username }}</li>
|
|
||||||
<li *ngIf="this.replayData.osu_replay_id">Link to score: <a class="btn" href="https://osu.ppy.sh/scores/osu/{{ this.replayData.osu_replay_id }}" target="_blank">osu!web</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div style="flex: 1; padding-left: 10px;">
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Max combo: {{ this.replayData.max_combo }}x
|
|
||||||
<span *ngIf="this.replayData.perfect" class="badge badge-green">perfect</span>
|
|
||||||
</li>
|
|
||||||
<li>Accuracy: {{ calculateAccuracy(this.replayData) | number: '1.2-2' }}%</li>
|
|
||||||
<li>300x: {{ this.replayData.count_300 }}</li>
|
|
||||||
<li>100x: {{ this.replayData.count_100 }}</li>
|
|
||||||
<li>50x: {{ this.replayData.count_50 }}</li>
|
|
||||||
<li>Misses: {{ this.replayData.count_miss }}</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="text-center mb-4 flex-container">
|
|
||||||
|
|
||||||
<div *ngIf="this.replayData.ur">
|
|
||||||
<h2 title="Converted Unstable Rate">cvUR</h2>
|
|
||||||
<div>{{ this.replayData.ur | number: '1.2-2' }}</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="this.replayData.adjusted_ur">
|
|
||||||
<h2 title="Adjusted cvUR - filters outlier hits">Adj. cvUR</h2>
|
|
||||||
<div>{{ this.replayData.adjusted_ur | number: '1.2-2' }}</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="this.replayData.frametime">
|
|
||||||
<h2 title="Median time between frames">Frametime</h2>
|
|
||||||
<div>{{ this.replayData.frametime | number: '1.0-2' }}ms</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="this.replayData.hits">
|
|
||||||
<h2 title="Hits within <1px of the edge">Edge Hits</h2>
|
|
||||||
<div>{{ this.replayData.hits }}</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="this.replayData.snaps">
|
|
||||||
<h2 title="Unusual snaps in the cursor movement">Snaps</h2>
|
|
||||||
<div>{{ this.replayData.snaps }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main term mb-2" *ngIf="this.replayData.similar_scores && this.replayData.similar_scores.length > 0">
|
|
||||||
<h1># similar replays</h1>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<th class="text-center">Played by</th>
|
|
||||||
<th class="text-center">PP</th>
|
|
||||||
<th class="text-center">Date</th>
|
|
||||||
<th class="text-center">Similarity</th>
|
|
||||||
<th class="text-center">Correlation</th>
|
|
||||||
<th class="text-center"></th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let score of this.replayData.similar_scores">
|
|
||||||
<td class="text-center">
|
|
||||||
<a [routerLink]="['/u/' + score.username]">{{ score.username }}</a>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{{ score.pp | number: '1.2-2' }}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{{ score.date }}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{{ score.similarity | number: '1.2-3' }}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{{ score.correlation | number: '1.2-4' }}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<a class="btn" [routerLink]="['/s/' + score.replay_id]">details</a>
|
|
||||||
</td>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main term mb-2" *ngIf="this.replayData.mean_error">
|
|
||||||
<h1># nerd stats</h1>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<th></th>
|
|
||||||
<th>
|
|
||||||
this replay
|
|
||||||
</th>
|
|
||||||
<th *ngIf="this.replayData.comparable_samples">
|
|
||||||
<span title="average values for this beatmap">
|
|
||||||
avg. (n={{ this.replayData.comparable_samples }})
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
</thead>
|
|
||||||
<tr>
|
|
||||||
<td>Mean error</td>
|
|
||||||
<td class="text-center">{{ this.replayData.mean_error | number: '1.2-2' }}</td>
|
|
||||||
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_mean_error | number: '1.2-2' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Error variance</td>
|
|
||||||
<td class="text-center">{{ this.replayData.error_variance | number: '1.2-2'}}</td>
|
|
||||||
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_variance | number: '1.2-2' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Error Std. deviation</td>
|
|
||||||
<td class="text-center">{{ this.replayData.error_standard_deviation | number: '1.2-2'}}</td>
|
|
||||||
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_standard_deviation | number: '1.2-2' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Min/max error</td>
|
|
||||||
<td class="text-center">[{{ this.replayData.minimum_error | number: '1.0-0' }}, {{ this.replayData.maximum_error | number: '1.0-0' }}]</td>
|
|
||||||
<td *ngIf="this.replayData.comparable_samples" class="text-center">[{{ this.replayData.comparable_minimum_error | number: '1.0-0' }}, {{ this.replayData.comparable_maximum_error | number: '1.0-0' }}]</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Coefficient of variation</td>
|
|
||||||
<td class="text-center">{{ this.replayData.error_coefficient_of_variation | number: '1.2-2'}}</td>
|
|
||||||
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_coefficient_of_variation | number: '1.2-2' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Kurtosis</td>
|
|
||||||
<td class="text-center">{{ this.replayData.error_kurtosis | number: '1.2-2'}}</td>
|
|
||||||
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_kurtosis | number: '1.2-2' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Error skewness</td>
|
|
||||||
<td class="text-center">{{ this.replayData.error_skewness | number: '1.2-2'}}</td>
|
|
||||||
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_skewness | number: '1.2-2' }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-container *ngFor="let chart of this.replayData.charts">
|
|
||||||
<app-chart [title]="chart.title" [data]="chart.data"></app-chart>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<div class="main term mb-2" *ngIf="this.replayData.error_distribution && Object.keys(this.replayData.error_distribution).length > 0">
|
|
||||||
<h1># hit distribution</h1>
|
|
||||||
<canvas baseChart
|
|
||||||
[data]="barChartData"
|
|
||||||
[options]="barChartOptions"
|
|
||||||
[plugins]="barChartPlugins"
|
|
||||||
[legend]="barChartLegend"
|
|
||||||
[type]="'bar'"
|
|
||||||
class="chart">
|
|
||||||
</canvas>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
|
||||||
import {HttpClient} from "@angular/common/http";
|
|
||||||
import {ActivatedRoute, RouterLink} from "@angular/router";
|
|
||||||
import {Title} from "@angular/platform-browser";
|
|
||||||
import {DistributionEntry, UserReplayData} from "../replays";
|
|
||||||
import {environment} from "../../environments/environment";
|
|
||||||
import {catchError, throwError} from "rxjs";
|
|
||||||
import {ChartComponent} from "../../corelib/components/chart/chart.component";
|
|
||||||
import {BaseChartDirective, NgChartsModule} from "ng2-charts";
|
|
||||||
import {DecimalPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
|
|
||||||
import {calculateAccuracy} from "../format";
|
|
||||||
import {ChartConfiguration} from "chart.js";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-view-user-score',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
ChartComponent,
|
|
||||||
NgChartsModule,
|
|
||||||
NgIf,
|
|
||||||
NgForOf,
|
|
||||||
RouterLink,
|
|
||||||
DecimalPipe,
|
|
||||||
NgOptimizedImage
|
|
||||||
],
|
|
||||||
templateUrl: './view-user-score.component.html',
|
|
||||||
styleUrl: './view-user-score.component.css'
|
|
||||||
})
|
|
||||||
export class ViewUserScoreComponent implements OnInit {
|
|
||||||
|
|
||||||
@ViewChild(BaseChartDirective)
|
|
||||||
public chart!: BaseChartDirective;
|
|
||||||
|
|
||||||
isLoading = false;
|
|
||||||
error: string | null = null;
|
|
||||||
replayData: UserReplayData | null = null;
|
|
||||||
replayId: number | null = null;
|
|
||||||
|
|
||||||
public barChartLegend = true;
|
|
||||||
public barChartPlugins = [];
|
|
||||||
|
|
||||||
public barChartData: ChartConfiguration<'bar'>['data'] = {
|
|
||||||
labels: [],
|
|
||||||
datasets: [
|
|
||||||
{ data: [], label: 'Miss (%)', backgroundColor: 'rgba(255,0,0,0.66)', borderRadius: 5 },
|
|
||||||
{ data: [], label: '50 (%)', backgroundColor: 'rgba(187,129,33,0.66)', borderRadius: 5 },
|
|
||||||
{ data: [], label: '100 (%)', backgroundColor: 'rgba(219,255,0,0.8)', borderRadius: 5 },
|
|
||||||
{ data: [], label: '300 (%)', backgroundColor: 'rgba(0,255,41,0.66)', borderRadius: 5 }
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
public barChartOptions: ChartConfiguration<'bar'>['options'] = {
|
|
||||||
responsive: true,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private http: HttpClient,
|
|
||||||
private activatedRoute: ActivatedRoute,
|
|
||||||
private title: Title
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.activatedRoute.params.subscribe(params => {
|
|
||||||
this.replayId = params['replayId'];
|
|
||||||
if (this.replayId) {
|
|
||||||
this.loadScoreData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadScoreData(): void {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.http.get<UserReplayData>(`${environment.apiUrl}/user-score/${this.replayId}`).pipe(
|
|
||||||
catchError(error => {
|
|
||||||
this.isLoading = false;
|
|
||||||
return throwError(() => new Error('An error occurred with the request: ' + error.message));
|
|
||||||
})
|
|
||||||
).subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.replayData = response;
|
|
||||||
|
|
||||||
this.title.setTitle(
|
|
||||||
`${this.replayData.username} on ${this.replayData.beatmap_title} (${this.replayData.beatmap_version})`
|
|
||||||
)
|
|
||||||
|
|
||||||
let errorDistribution = Object.entries(this.replayData.error_distribution);
|
|
||||||
|
|
||||||
if (errorDistribution.length >= 1) {
|
|
||||||
const sortedEntries = errorDistribution
|
|
||||||
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
|
|
||||||
|
|
||||||
const chartData = this.generateChartData(sortedEntries);
|
|
||||||
this.barChartData.labels = chartData.labels;
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
this.barChartData.datasets[i].data = chartData.datasets[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getChartRange(entries: [string, DistributionEntry][]): [number, number] {
|
|
||||||
const keys = entries.map(([key, _]) => parseInt(key));
|
|
||||||
|
|
||||||
const minKey = Math.min(...keys);
|
|
||||||
const maxKey = Math.max(...keys);
|
|
||||||
|
|
||||||
return [minKey, maxKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateChartData(entries: [string, DistributionEntry][]): { labels: string[], datasets: number[][] } {
|
|
||||||
const range = this.getChartRange(entries);
|
|
||||||
const labels: string[] = [];
|
|
||||||
const datasets: number[][] = Array(4).fill(0).map(() => []);
|
|
||||||
|
|
||||||
const defaultPercentageValues: DistributionEntry = {
|
|
||||||
countMiss: 0,
|
|
||||||
count50: 0,
|
|
||||||
count100: 0,
|
|
||||||
count300: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const entriesMap = new Map<number, DistributionEntry>(entries.map(([key, value]) => [parseInt(key), value]));
|
|
||||||
|
|
||||||
for (let key = range[0]; key <= range[1]; key += 2) {
|
|
||||||
const endKey = key + 2 <= range[1] ? key + 2 : key + 1;
|
|
||||||
labels.push(`${key}ms to ${endKey}ms`);
|
|
||||||
|
|
||||||
const currentEntry = entriesMap.get(key) || { ...defaultPercentageValues };
|
|
||||||
const nextEntry = key + 1 <= range[1] ? (entriesMap.get(key + 1) || { ...defaultPercentageValues }) : defaultPercentageValues;
|
|
||||||
|
|
||||||
const sumEntry: DistributionEntry = {
|
|
||||||
countMiss: currentEntry.countMiss + nextEntry.countMiss,
|
|
||||||
count50: currentEntry.count50 + nextEntry.count50,
|
|
||||||
count100: currentEntry.count100 + nextEntry.count100,
|
|
||||||
count300: currentEntry.count300 + nextEntry.count300,
|
|
||||||
};
|
|
||||||
|
|
||||||
datasets[0].push(sumEntry.countMiss);
|
|
||||||
datasets[1].push(sumEntry.count50);
|
|
||||||
datasets[2].push(sumEntry.count100);
|
|
||||||
datasets[3].push(sumEntry.count300);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handling the case for an odd last key if needed
|
|
||||||
if (range[1] % 2 !== range[0] % 2) {
|
|
||||||
const lastEntry = entriesMap.get(range[1]) || { ...defaultPercentageValues };
|
|
||||||
labels.push(`${range[1]}ms to ${range[1] + 1}ms`);
|
|
||||||
datasets[0].push(lastEntry.countMiss);
|
|
||||||
datasets[1].push(lastEntry.count50);
|
|
||||||
datasets[2].push(lastEntry.count100);
|
|
||||||
datasets[3].push(lastEntry.count300);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { labels, datasets };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly Object = Object;
|
|
||||||
protected readonly calculateAccuracy = calculateAccuracy;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user