292 lines
9.6 KiB
Scala
292 lines
9.6 KiB
Scala
package views.html.user
|
|
|
|
import lila.api.Context
|
|
import lila.app.templating.Environment._
|
|
import lila.app.ui.ScalatagsTemplate._
|
|
import lila.common.String.html.safeJsonValue
|
|
import lila.rating.{ Perf, PerfType }
|
|
import lila.perfStat.PerfStat
|
|
import lila.user.User
|
|
|
|
import controllers.routes
|
|
|
|
object perfStat {
|
|
|
|
def apply(
|
|
u: User,
|
|
rankMap: lila.rating.UserRankMap,
|
|
perfType: lila.rating.PerfType,
|
|
percentile: Option[Double],
|
|
stat: PerfStat,
|
|
ratingChart: Option[String]
|
|
)(implicit ctx: Context) = views.html.base.layout(
|
|
title = s"${u.username} ${perfType.name} stats",
|
|
robots = false,
|
|
moreJs = frag(
|
|
jsAt("compiled/user.js"),
|
|
ratingChart.map { rc =>
|
|
frag(
|
|
jsTag("chart/ratingHistory.js"),
|
|
embedJsUnsafe(s"lichess.ratingHistoryChart($rc,'${perfType.name}');")
|
|
)
|
|
}
|
|
),
|
|
moreCss = cssTag("perf-stat")
|
|
) {
|
|
main(cls := s"page-menu")(
|
|
st.aside(cls := "page-menu__menu")(show.side(u, rankMap, perfType.some)),
|
|
div(cls := s"page-menu__content box perf-stat ${perfType.key}")(
|
|
div(cls := "box__top")(
|
|
h1(
|
|
a(href := routes.User.show(u.username))(u.username),
|
|
span(perfType.name, " stats")
|
|
),
|
|
div(cls := "box__top__actions")(
|
|
u.perfs(perfType).nb > 0 option a(
|
|
cls := "button button-empty text",
|
|
dataIcon := perfType.iconChar,
|
|
href := s"${routes.User.games(u.username, "search")}?perf=${perfType.id}"
|
|
)("View the games"),
|
|
bits.perfTrophies(u, rankMap.filterKeys(perfType==))
|
|
)
|
|
),
|
|
ratingChart.isDefined option div(cls := "rating-history")(spinner),
|
|
div(cls := "box__pad perf-stat__content")(
|
|
glicko(perfType, u.perfs(perfType), percentile),
|
|
counter(stat.count),
|
|
highlow(stat),
|
|
resultStreak(stat.resultStreak),
|
|
result(stat),
|
|
playStreakNb(stat.playStreak),
|
|
playStreakTime(stat.playStreak)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
private def decimal(v: Double) = lila.common.Maths.roundAt(v, 2)
|
|
|
|
private def glicko(perfType: PerfType, perf: Perf, percentile: Option[Double]): Frag = st.section(cls := "glicko")(
|
|
h2(
|
|
"Rating: ",
|
|
strong(title := "Yes, ratings have decimal accuracy.")(decimal(perf.glicko.rating).toString),
|
|
". ",
|
|
span(cls := "details")(
|
|
perf.glicko.provisional option span(title := "Not enough rated games have been played to establish a reliable rating.")("(provisional)"),
|
|
percentile.filter(_ != 0.0 && !perf.glicko.provisional) map { percentile =>
|
|
frag(
|
|
"Better than ",
|
|
a(href := routes.Stat.ratingDistribution(perfType.key))(
|
|
strong(percentile, "%"), " of ", perfType.name, " players"
|
|
),
|
|
"."
|
|
)
|
|
}
|
|
)
|
|
),
|
|
p(
|
|
"Progression over the last twelve games: ",
|
|
span(cls := "progress")(
|
|
if (perf.progress > 0) tag("green")(dataIcon := "N")(perf.progress)
|
|
else if (perf.progress < 0) tag("red")(dataIcon := "M")(-perf.progress)
|
|
else frag("none")
|
|
),
|
|
". ",
|
|
"Rating deviation: ",
|
|
strong(title := "Lower value means the rating is more stable. Above 110, the rating is considered provisional.")(decimal(perf.glicko.deviation).toString),
|
|
"."
|
|
)
|
|
)
|
|
|
|
private def pct(num: Int, denom: Int): String = {
|
|
if (denom == 0) "0"
|
|
else s"${Math.round(num * 100 / denom)}%"
|
|
}
|
|
|
|
private def counter(count: lila.perfStat.Count): Frag = st.section(cls := "counter split")(
|
|
div(
|
|
table(
|
|
tbody(
|
|
tr(
|
|
th("Total games"),
|
|
td(count.all),
|
|
td
|
|
),
|
|
tr(cls := "full")(
|
|
th("Rated games"),
|
|
td(count.rated),
|
|
td(pct(count.rated, count.all))
|
|
),
|
|
tr(cls := "full")(
|
|
th("Tournament games"),
|
|
td(count.tour),
|
|
td(pct(count.tour, count.all))
|
|
),
|
|
tr(cls := "full")(
|
|
th("Berserked games"),
|
|
td(count.berserk),
|
|
td(pct(count.berserk, count.tour))
|
|
),
|
|
count.seconds > 0 option tr(cls := "full")(
|
|
th("Time spent playing"),
|
|
td(colspan := "2") {
|
|
val hours = count.seconds / (60 * 60)
|
|
val minutes = (count.seconds % (60 * 60)) / 60
|
|
s"${hours}h, ${minutes}m"
|
|
}
|
|
)
|
|
)
|
|
)
|
|
),
|
|
div(
|
|
table(
|
|
tbody(
|
|
tr(
|
|
th("Average opponent"),
|
|
td(decimal(count.opAvg.avg).toString),
|
|
td
|
|
),
|
|
tr(cls := "full")(
|
|
th("Victories"),
|
|
td(tag("green")(count.win)),
|
|
td(tag("green")(pct(count.win, count.all)))
|
|
),
|
|
tr(cls := "full")(
|
|
th("Draws"),
|
|
td(count.draw),
|
|
td(pct(count.draw, count.all))
|
|
),
|
|
tr(cls := "full")(
|
|
th("Defeats"),
|
|
td(tag("red")(count.loss)),
|
|
td(tag("red")(pct(count.loss, count.all)))
|
|
),
|
|
tr(cls := "full")(
|
|
th("Disconnections"),
|
|
td(if (count.disconnects > count.all * 100 / 15) tag("red") else frag())(count.disconnects),
|
|
td(pct(count.disconnects, count.all))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
private def highlowSide(title: String, opt: Option[lila.perfStat.RatingAt], color: String)(implicit ctx: Context): Frag =
|
|
opt match {
|
|
case Some(r) => div(
|
|
h2(title, ": ", strong(tag(color)(r.int))),
|
|
a(cls := "glpt", href := routes.Round.watcher(r.gameId, "white"))(absClientDateTime(r.at))
|
|
)
|
|
case None => div(h2(title), " ", span("Not enough games played"))
|
|
}
|
|
|
|
private def highlow(stat: PerfStat)(implicit ctx: Context): Frag = st.section(cls := "highlow split")(
|
|
highlowSide("Highest rating", stat.highest, "green"),
|
|
highlowSide("Lowest rating", stat.lowest, "red")
|
|
)
|
|
|
|
private def fromTo(s: lila.perfStat.Streak)(implicit ctx: Context): Frag =
|
|
s.from match {
|
|
case Some(from) => frag(
|
|
"from ",
|
|
a(cls := "glpt", href := routes.Round.watcher(from.gameId, "white"))(absClientDateTime(from.at)),
|
|
" to ",
|
|
s.to match {
|
|
case Some(to) => a(cls := "glpt", href := routes.Round.watcher(to.gameId, "white"))(absClientDateTime(to.at))
|
|
case None => frag("now")
|
|
}
|
|
)
|
|
case None => nbsp
|
|
}
|
|
|
|
private def resultStreakSideStreak(s: lila.perfStat.Streak, title: String, color: String)(implicit ctx: Context): Frag = div(cls := "streak")(
|
|
h3(
|
|
title, ": ",
|
|
if (s.v == 1) tag(color)(frag(strong(s.v), " game"))
|
|
else if (s.v > 0) tag(color)(frag(strong(s.v), " games"))
|
|
else frag("none")
|
|
),
|
|
fromTo(s)
|
|
)
|
|
|
|
private def resultStreakSide(s: lila.perfStat.Streaks, title: String, color: String)(implicit ctx: Context): Frag = div(
|
|
h2(title),
|
|
resultStreakSideStreak(s.max, "Longest", color),
|
|
resultStreakSideStreak(s.cur, "Current", color)
|
|
)
|
|
|
|
private def resultStreak(streak: lila.perfStat.ResultStreak)(implicit ctx: Context): Frag = st.section(cls := "resultStreak split")(
|
|
resultStreakSide(streak.win, "Winning streak", "green"),
|
|
resultStreakSide(streak.loss, "Losing streak", "red")
|
|
)
|
|
|
|
private def resultTable(results: lila.perfStat.Results, title: String)(implicit ctx: Context): Frag = div(
|
|
table(
|
|
thead(
|
|
tr(
|
|
th(colspan := 2)(h2(title))
|
|
)
|
|
),
|
|
tbody(
|
|
results.results map { r =>
|
|
tr(
|
|
td(userIdLink(r.opId.value.some, withOnline = false), " (", r.opInt, ")"),
|
|
td(a(cls := "glpt", href := routes.Round.watcher(r.gameId, "white"))(absClientDateTime(r.at)))
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
|
|
private def result(stat: PerfStat)(implicit ctx: Context): Frag = st.section(cls := "result split")(
|
|
resultTable(stat.bestWins, "Best rated victories"),
|
|
resultTable(stat.worstLosses, "Worst rated defeats")
|
|
)
|
|
|
|
private def playStreakNbStreak(s: lila.perfStat.Streak, title: String)(implicit ctx: Context): Frag = div(
|
|
div(cls := "streak")(
|
|
h3(
|
|
title, ": ",
|
|
if (s.v == 1) frag(strong(s.v), " game")
|
|
else if (s.v > 0) frag(strong(s.v), " games")
|
|
else frag("none")
|
|
),
|
|
fromTo(s)
|
|
)
|
|
)
|
|
|
|
private def playStreakNbStreaks(streaks: lila.perfStat.Streaks)(implicit ctx: Context): Frag = div(cls := "split")(
|
|
playStreakNbStreak(streaks.max, "Longest streak"),
|
|
playStreakNbStreak(streaks.cur, "Current streak")
|
|
)
|
|
|
|
private def playStreakNb(playStreak: lila.perfStat.PlayStreak)(implicit ctx: Context): Frag = st.section(cls := "playStreak")(
|
|
h2(span(title := "Less than one hour between games")("Games played in a row")),
|
|
playStreakNbStreaks(playStreak.nb)
|
|
)
|
|
|
|
private def playStreakTimeStreak(s: lila.perfStat.Streak, title: String)(implicit ctx: Context): Frag = div(
|
|
div(cls := "streak")(
|
|
h3(
|
|
title, ": ", {
|
|
val hours = s.v / (60 * 60)
|
|
val minutes = (s.v % (60 * 60)) / 60
|
|
s"${hours} hours, ${minutes} minutes"
|
|
}
|
|
),
|
|
fromTo(s)
|
|
)
|
|
)
|
|
|
|
private def playStreakTimeStreaks(streaks: lila.perfStat.Streaks)(implicit ctx: Context): Frag = div(cls := "split")(
|
|
playStreakTimeStreak(streaks.max, "Longest streak"),
|
|
playStreakTimeStreak(streaks.cur, "Current streak")
|
|
)
|
|
|
|
private def playStreakTime(playStreak: lila.perfStat.PlayStreak)(implicit ctx: Context): Frag = st.section(cls := "playStreak")(
|
|
h2(span(title := "Less than one hour between games")("Max time spent playing")),
|
|
playStreakTimeStreaks(playStreak.time)
|
|
|
|
)
|
|
}
|