moth

Monarch Of The Hill game server
git clone https://git.woozle.org/neale/moth.git

moth / theme
Neale Pickett  ·  2023-11-16

scoreboard.mjs

  1import * as moth from "./moth.mjs"
  2import * as common from "./common.mjs"
  3
  4const server = new moth.Server(".")
  5/** Don't let any team's score exceed this percentage width */
  6const MaxScoreWidth = 90
  7
  8/**
  9 * Returns a promise that resolves after timeout.
 10 *
 11 * This uses setTimeout instead of some other fancy thing like
 12 * requestAnimationFrame, because who actually cares about scoreboard update
 13 * framerate?
 14 *
 15 * @param {Number} timeout How long to sleep (milliseconds)
 16 * @returns {Promise}
 17 */
 18function sleep(timeout) {
 19  return new Promise(resolve => setTimeout(resolve, timeout));
 20}
 21
 22/**
 23 * Pull new points log, and update the scoreboard.
 24 * 
 25 * The update is animated, because I think that looks cool.
 26 */
 27async function update() {
 28  let config = {}
 29  try {
 30    config = await common.Config()
 31  }
 32  catch (err) {
 33    console.warn("Parsing config.json:", err)
 34  }
 35
 36  // Pull configuration settings
 37  if (!config.Scoreboard) {
 38    console.warn("config.json has empty Scoreboard section")
 39  }
 40  let ScoreboardConfig = config.Scoreboard ?? {}
 41  let state = await server.GetState()
 42
 43  // Show URL of server
 44  for (let e of document.querySelectorAll(".location")) {
 45    e.textContent = common.BaseURL
 46    e.classList.toggle("hidden", !(ScoreboardConfig.DisplayServerURLWhenEnabled && state.Enabled))
 47  }
 48
 49  // Rotate views
 50  for (let e of document.querySelectorAll(".rotate")) {
 51    e.appendChild(e.firstChild)
 52  }
 53
 54  // Render rankings
 55  for (let e of document.querySelectorAll(".rankings")) {
 56    if (e.classList.contains("classic")) {
 57      classicRankings(e, state, ScoreboardConfig)
 58    } else if (e.classList.contains("category")) {
 59      categoryRankings(e, state, ScoreboardConfig)
 60    }
 61  }
 62
 63}
 64
 65async function classicRankings(rankingsElement, state, ScoreboardConfig) {
 66  let ReplayHistory = ScoreboardConfig.ReplayHistory ?? false
 67  let ReplayDurationMS = ScoreboardConfig.ReplayDurationMS ?? 300
 68  let ReplayFPS = ScoreboardConfig.ReplayFPS ?? 24
 69
 70  let logSize = state.PointsLog.length
 71
 72  // Figure out the timing so that we can replay the scoreboard in about
 73  // ReplayDurationMS.
 74  let frameModulo = 1
 75  let delay = 0
 76  while (delay < (common.Second / ReplayFPS)) {
 77    frameModulo += 1
 78    delay = ReplayDurationMS / (logSize / frameModulo)
 79  }
 80
 81  let frame = 0
 82  for (let scores of state.ScoresHistory()) {
 83    frame += 1
 84    if (frame < state.PointsLog.length) { // Always render the last frame
 85      if (!ReplayHistory || (frame % frameModulo)) { // Skip if we're not animating, or if we need to drop frames
 86        continue
 87      }
 88    }
 89
 90    while (rankingsElement.firstChild) rankingsElement.firstChild.remove()
 91
 92    let sortedTeamIDs = [...scores.TeamIDs]
 93    sortedTeamIDs.sort((a, b) => scores.CyFiScore(a) - scores.CyFiScore(b))
 94    sortedTeamIDs.reverse()
 95    
 96    let topScore = scores.CyFiScore(sortedTeamIDs[0])
 97    for (let teamID of sortedTeamIDs) {
 98      let teamName = state.TeamNames[teamID] ?? "rodney"
 99      
100      let row = rankingsElement.appendChild(document.createElement("div"))
101      
102      let teamname = row.appendChild(document.createElement("span"))
103      teamname.textContent = teamName
104      teamname.classList.add("teamname")
105      
106      let teampoints = row.appendChild(document.createElement("span"))
107      teampoints.classList.add("teampoints")
108      for (let category of scores.Categories) {
109        let score = scores.CyFiCategoryScore(category, teamID)
110        if (!score) {
111          continue
112        }
113
114        // XXX: Figure out how to do this properly with flexbox
115        let block = row.appendChild(document.createElement("span"))
116        let points = scores.GetPoints(category, teamID)
117        let width = MaxScoreWidth * score / topScore
118        let categoryNumber = [...scores.Categories].indexOf(category)
119
120        block.textContent = category
121        block.title = `${points} points`
122        block.style.width = `${width}%`
123        block.classList.add("category", `cat${categoryNumber}`)
124        block.classList.toggle("topscore", (score == 1) && ScoreboardConfig.ShowCategoryLeaders)
125
126        categoryNumber += 1
127      } 
128    }
129    await sleep(delay)
130  }
131
132  for (let e of document.querySelectorAll(".no-scores")) {
133    e.innerHTML = ScoreboardConfig.NoScoresHtml
134    e.classList.toggle("hidden", frame > 0)
135  }
136}
137
138/**
139 * 
140 * @param {*} rankingsElement 
141 * @param {moth.State} state 
142 * @param {*} ScoreboardConfig 
143 */
144async function categoryRankings(rankingsElement, state, ScoreboardConfig) {
145  while (rankingsElement.firstChild) rankingsElement.firstChild.remove()
146  let scores = state.CurrentScores()
147  for (let category of scores.Categories) {
148    let categoryBox = rankingsElement.appendChild(document.createElement("div"))
149    categoryBox.classList.add("category")
150
151    categoryBox.appendChild(document.createElement("h2")).textContent = category
152
153    let categoryScores = []
154    for (let teamID in state.TeamNames) {
155      categoryScores.push({
156        teamName: state.TeamNames[teamID],
157        score: scores.GetPoints(category, teamID),
158      })
159    }
160    categoryScores.sort((a, b) => b.score - a.score)
161    
162    let table = categoryBox.appendChild(document.createElement("table"))
163    let rows = 0
164    for (let categoryScore of categoryScores) {
165      let row = table.appendChild(document.createElement("tr"))
166      row.appendChild(document.createElement("td")).textContent = categoryScore.teamName
167      let td = row.appendChild(document.createElement("td"))
168      td.textContent = categoryScore.score
169      td.classList.add("number")
170      rows += 1
171      if (rows == 5) {
172        break
173      }
174    }
175  }
176}
177
178function init() {
179  setInterval(update, common.Minute)
180  update()
181}
182
183common.WhenDOMLoaded(init)