mirror of https://github.com/dirtbags/moth.git
Compare commits
6 Commits
6045000564
...
63881f05fa
Author | SHA1 | Date |
---|---|---|
Neale Pickett | 63881f05fa | |
Neale Pickett | c4bf25f8fa | |
Neale Pickett | 610eb27430 | |
Neale Pickett | e4a8883f27 | |
Neale Pickett | 79cef80486 | |
Neale Pickett | 62043919f5 |
|
@ -1,11 +1,12 @@
|
||||||
{
|
{
|
||||||
"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) -->",
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
|
@ -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>
|
|
@ -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;}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue