Compare commits

...

6 Commits

Author SHA1 Message Date
Neale Pickett 63881f05fa New scoreboard view 2023-11-16 23:44:32 -07:00
Neale Pickett c4bf25f8fa s/id/class/ 2023-11-16 22:37:05 -07:00
Neale Pickett 610eb27430 Scoreboard changes:
* Consistent category colors
* Only show server URL when enabled
* HTML to display when there are no scores
2023-11-16 22:18:16 -07:00
Neale Pickett e4a8883f27 Scoreboard: preserve category order 2023-11-16 20:07:49 -07:00
Neale Pickett 79cef80486 scoreboard: category stays consistent color 2023-11-16 19:57:01 -07:00
Neale Pickett 62043919f5 Reduce scoreboard replay FPS 2023-11-16 19:56:30 -07:00
6 changed files with 150 additions and 33 deletions

View File

@ -1,13 +1,14 @@
{ {
"TrackSolved": true, "TrackSolved": true,
"Scoreboard": { "Scoreboard": {
"DisplayServerURL": true, "DisplayServerURLWhenEnabled": true,
"ShowCategoryLeaders": true, "ShowCategoryLeaders": true,
"ReplayHistory": true, "ReplayHistory": true,
"ReplayFPS": 30, "ReplayFPS": 6,
"ReplayDurationMS": 2000, "ReplayDurationMS": 2000,
"NoScoresHtml": "<div class='notification'><h2>~ no scores ~</h2></div>",
"": "" "": ""
}, },
"Messages": "<!-- Messages can go here (HTML) -->", "Messages": "<!-- Messages can go here (HTML) -->",
"": "this is here so you don't have to remember to take the comma off the last item" "": "this is here so you don't have to remember to take the comma off the last item"
} }

View File

@ -367,8 +367,8 @@ class State {
Devel: obj.Config.Devel, Devel: obj.Config.Devel,
} }
/** True if the server is in enabled state */ /** True if the server is in enabled state, or if we don't know */
this.Enabled = obj.Enabled this.Enabled = obj.Enabled ?? true
/** Map from Team ID to Team Name /** Map from Team ID to Team Name
* @type {Object.<string,string>} * @type {Object.<string,string>}
@ -492,9 +492,7 @@ class State {
* @returns {Scores} * @returns {Scores}
*/ */
CurrentScores() { CurrentScores() {
let scores return [...this.ScoresHistory()].pop()
for (scores of this.ScoreHistory());
return scores
} }
} }
@ -676,4 +674,5 @@ class Server {
export { export {
Hash, Hash,
Server, Server,
State,
} }

19
theme/scoreboard-all.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Scoreboard</title>
<link rel="stylesheet" href="basic.css">
<link rel="stylesheet" href="scoreboard.css">
<meta name="viewport" content="width=device-width">
<link rel="icon" href="luna-moth.svg">
<script type="module" src="scoreboard.mjs"></script>
</head>
<body>
<div class="no-scores hidden"></div>
<div class="rotate">
<div class="rankings category"></div>
<div class="rankings classic"></div>
</div>
<div class="location"></div>
</body>
</html>

View File

@ -18,32 +18,52 @@
text-decoration: underline; text-decoration: underline;
} }
.no-scores {
display: flex;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
min-height: calc(100vh - 2em);
}
.no-scores.hidden {
display: none;
}
.no-scores img {
object-fit: cover;
max-height: 60vh;
}
/* Only the first child of a rotate class is visible */
.rotate > div:nth-child(n + 2) {
display: none;
}
/** Scoreboard */ /** Scoreboard */
#rankings { .rankings.classic {
width: 100%; width: 100%;
position: relative; position: relative;
background-color: #000c; background-color: #000c;
} }
#rankings div { .rankings.classic div {
height: 1.2rem; height: 1.2rem;
display: flex; display: flex;
align-items: center; align-items: center;
} }
#rankings div:nth-child(6n){ .rankings.classic div:nth-child(6n){
background-color: #ccc3; background-color: #ccc3;
} }
#rankings div:nth-child(6n+3) { .rankings.classic div:nth-child(6n+3) {
background-color: #0f03; background-color: #0f03;
} }
#rankings span { .rankings.classic span {
display: inline-block; display: inline-block;
overflow: hidden; overflow: hidden;
} }
#rankings span.category { .rankings.classic span.category {
font-size: 80%; font-size: 80%;
} }
#rankings span.teamname { .rankings.classic span.teamname {
height: auto; height: auto;
font-size: inherit; font-size: inherit;
color: white; color: white;
@ -52,8 +72,8 @@
position: absolute; position: absolute;
right: 0.2em; right: 0.2em;
} }
#rankings span.teamname:hover, .rankings.classic span.teamname:hover,
#rankings span.category:hover { .rankings.classic span.category:hover {
width: inherit; width: inherit;
max-width: 100%; max-width: 100%;
} }
@ -63,8 +83,24 @@
vertical-align: top; vertical-align: top;
} }
.rankings.category {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
.rankings.category div {
border: solid black 2px;
min-width: 15em;
}
.rankings.category table {
width: 100%;
}
.rankings.category td.number {
text-align: right;
}
@media only screen and (max-width: 450px) { @media only screen and (max-width: 450px) {
#rankings span.teamname { .rankings.classic span.teamname {
max-width: 6em; max-width: 6em;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -73,7 +109,7 @@
} }
} }
#rankings div * {white-space: nowrap;} .rankings div * {white-space: nowrap;}
.cat0, .cat8, .cat16 {background-color: #a6cee3; color: black;} .cat0, .cat8, .cat16 {background-color: #a6cee3; color: black;}
.cat1, .cat9, .cat17 {background-color: #1f78b4; color: white;} .cat1, .cat9, .cat17 {background-color: #1f78b4; color: white;}
.cat2, .cat10, .cat18 {background-color: #b2df8a; color: black;} .cat2, .cat10, .cat18 {background-color: #b2df8a; color: black;}

View File

@ -5,13 +5,12 @@
<link rel="stylesheet" href="basic.css"> <link rel="stylesheet" href="basic.css">
<link rel="stylesheet" href="scoreboard.css"> <link rel="stylesheet" href="scoreboard.css">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<script src="https://cdn.jsdelivr.net/npm/luxon@1.26.0"></script> <link rel="icon" href="luna-moth.svg">
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.0.2"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@0.2.1"></script>
<script type="module" src="scoreboard.mjs"></script> <script type="module" src="scoreboard.mjs"></script>
</head> </head>
<body> <body>
<div id="rankings"></div> <div class="no-scores hidden"></div>
<div class="rankings classic"></div>
<div class="location"></div> <div class="location"></div>
</body> </body>
</html> </html>

View File

@ -34,25 +34,43 @@ async function update() {
} }
// Pull configuration settings // Pull configuration settings
let ScoreboardConfig = config.Scoreboard ?? {}
let ReplayHistory = ScoreboardConfig.ReplayHistory ?? false
let ReplayDurationMS = ScoreboardConfig.ReplayDurationMS ?? 300
let ReplayFPS = ScoreboardConfig.ReplayFPS ?? 24
if (!config.Scoreboard) { if (!config.Scoreboard) {
console.warn("config.json has empty Scoreboard section") console.warn("config.json has empty Scoreboard section")
} }
let ScoreboardConfig = config.Scoreboard ?? {}
let state = await server.GetState()
// Show URL of server
for (let e of document.querySelectorAll(".location")) { for (let e of document.querySelectorAll(".location")) {
e.textContent = common.BaseURL e.textContent = common.BaseURL
e.classList.toggle("hidden", !ScoreboardConfig.DisplayServerURL) e.classList.toggle("hidden", !(ScoreboardConfig.DisplayServerURLWhenEnabled && state.Enabled))
} }
let state = await server.GetState() // Rotate views
let rankingsElement = document.querySelector("#rankings") for (let e of document.querySelectorAll(".rotate")) {
e.appendChild(e.firstChild)
}
// Render rankings
for (let e of document.querySelectorAll(".rankings")) {
if (e.classList.contains("classic")) {
classicRankings(e, state, ScoreboardConfig)
} else if (e.classList.contains("category")) {
categoryRankings(e, state, ScoreboardConfig)
}
}
}
async function classicRankings(rankingsElement, state, ScoreboardConfig) {
let ReplayHistory = ScoreboardConfig.ReplayHistory ?? false
let ReplayDurationMS = ScoreboardConfig.ReplayDurationMS ?? 300
let ReplayFPS = ScoreboardConfig.ReplayFPS ?? 24
let logSize = state.PointsLog.length let logSize = state.PointsLog.length
// Figure out the timing so that we can replay the scoreboard in about // Figure out the timing so that we can replay the scoreboard in about
// ReplayDurationMS, but no more than 24 frames per second. // ReplayDurationMS.
let frameModulo = 1 let frameModulo = 1
let delay = 0 let delay = 0
while (delay < (common.Second / ReplayFPS)) { while (delay < (common.Second / ReplayFPS)) {
@ -77,7 +95,7 @@ async function update() {
let topScore = scores.CyFiScore(sortedTeamIDs[0]) let topScore = scores.CyFiScore(sortedTeamIDs[0])
for (let teamID of sortedTeamIDs) { for (let teamID of sortedTeamIDs) {
let teamName = state.TeamNames[teamID] let teamName = state.TeamNames[teamID] ?? "rodney"
let row = rankingsElement.appendChild(document.createElement("div")) let row = rankingsElement.appendChild(document.createElement("div"))
@ -85,7 +103,6 @@ async function update() {
teamname.textContent = teamName teamname.textContent = teamName
teamname.classList.add("teamname") teamname.classList.add("teamname")
let categoryNumber = 0
let teampoints = row.appendChild(document.createElement("span")) let teampoints = row.appendChild(document.createElement("span"))
teampoints.classList.add("teampoints") teampoints.classList.add("teampoints")
for (let category of scores.Categories) { for (let category of scores.Categories) {
@ -98,6 +115,7 @@ async function update() {
let block = row.appendChild(document.createElement("span")) let block = row.appendChild(document.createElement("span"))
let points = scores.GetPoints(category, teamID) let points = scores.GetPoints(category, teamID)
let width = MaxScoreWidth * score / topScore let width = MaxScoreWidth * score / topScore
let categoryNumber = [...scores.Categories].indexOf(category)
block.textContent = category block.textContent = category
block.title = `${points} points` block.title = `${points} points`
@ -110,6 +128,51 @@ async function update() {
} }
await sleep(delay) await sleep(delay)
} }
for (let e of document.querySelectorAll(".no-scores")) {
e.innerHTML = ScoreboardConfig.NoScoresHtml
e.classList.toggle("hidden", frame > 0)
}
}
/**
*
* @param {*} rankingsElement
* @param {moth.State} state
* @param {*} ScoreboardConfig
*/
async function categoryRankings(rankingsElement, state, ScoreboardConfig) {
while (rankingsElement.firstChild) rankingsElement.firstChild.remove()
let scores = state.CurrentScores()
for (let category of scores.Categories) {
let categoryBox = rankingsElement.appendChild(document.createElement("div"))
categoryBox.classList.add("category")
categoryBox.appendChild(document.createElement("h2")).textContent = category
let categoryScores = []
for (let teamID in state.TeamNames) {
categoryScores.push({
teamName: state.TeamNames[teamID],
score: scores.GetPoints(category, teamID),
})
}
categoryScores.sort((a, b) => b.score - a.score)
let table = categoryBox.appendChild(document.createElement("table"))
let rows = 0
for (let categoryScore of categoryScores) {
let row = table.appendChild(document.createElement("tr"))
row.appendChild(document.createElement("td")).textContent = categoryScore.teamName
let td = row.appendChild(document.createElement("td"))
td.textContent = categoryScore.score
td.classList.add("number")
rows += 1
if (rows == 5) {
break
}
}
}
} }
function init() { function init() {