Neale Pickett
·
2024-12-05
replay.mjs
1import {Player} from "./player.mjs"
2
3const Millisecond = 1
4const Second = 1000 * Millisecond
5const Minute = 60 * Second
6
7const GameTime = new Intl.DateTimeFormat(
8 undefined,
9 {
10 dateStyle: "short",
11 timeStyle: "short",
12 },
13)
14
15function dateOfFn(fn) {
16 let ts = parseInt(fn.replace(/\.json$/, ""), 16) * Second
17 let when = new Date(ts)
18 return GameTime.format(when)
19}
20
21class Replay {
22 constructor(ctx, select, results, dateOutput, stats) {
23 this.ctx = ctx
24 this.select = select
25 this.results = results
26 this.dateOutput = dateOutput
27 this.stats = stats
28 this.player = new Player(ctx)
29 this.games = {}
30
31 select.addEventListener("change", () => this.reloadPlayer())
32 }
33
34 async refreshSingleGame(fn) {
35 if (this.games[fn]) {
36 return
37 }
38 let resp = await fetch(`rounds/${fn}`)
39 let game = await resp.json()
40 this.games[fn] = game
41 }
42
43 async refreshGames() {
44 let resp = await fetch("rounds/index.json")
45 let index = await resp.json()
46 let promises = []
47 for (let fn of index) {
48 let p = this.refreshSingleGame(fn)
49 promises.push(p)
50 }
51 for (let fn in this.games) {
52 if (!index.includes(fn)) {
53 delete this.games[fn]
54 }
55 }
56 await Promise.all(promises)
57 }
58
59 fns() {
60 return Object.keys(this.games).toSorted()
61 }
62
63 async refresh() {
64 await this.refreshGames()
65
66 let newGame = false
67 let latest = this.select.firstElementChild
68 for (let fn of this.fns()) {
69 let game = this.games[fn]
70 if (!game.element) {
71 game.element = document.createElement("option")
72 newGame = true
73 }
74 game.element.value = fn
75 game.element.textContent = dateOfFn(fn)
76
77 // This moves existing elements in Chrome.
78 // But the MDN document doesn't specify what happens with existing elements.
79 // So this may break somewhere.
80 latest.after(game.element)
81 }
82
83 // Remove anything no longer in games
84 for (let option of this.select.querySelectorAll("[value]")) {
85 let fn = option.value
86 if (!(fn in this.games)) {
87 option.remove()
88 }
89 }
90
91 if (newGame) {
92 this.reloadStats()
93 }
94
95 if (newGame && (this.select.value == "latest")) {
96 this.reloadPlayer()
97 }
98 }
99
100 reloadStats() {
101 let TankNames = {}
102 let TankColors = {}
103 let TotalGames = {}
104 let TotalKills = {}
105 let TotalDeaths = {}
106 let MaxKills = 0
107 let MaxDeaths = 0
108 let ngames = 0
109 for (let fn of this.fns()) {
110 let game = this.games[fn]
111 for (let tank of game.tanks) {
112 TotalGames[tank.uid] = (TotalGames[tank.uid] ?? 0) + 1
113 TotalKills[tank.uid] = (TotalKills[tank.uid] ?? 0) + tank.kills
114 TotalDeaths[tank.uid] = (TotalDeaths[tank.uid] ?? 0) + (tank.killer == -1 ? 0 : 1)
115 TankNames[tank.uid] = tank.name
116 TankColors[tank.uid] = tank.color
117
118 MaxKills = Math.max(MaxKills, TotalKills[tank.uid])
119 MaxDeaths = Math.max(MaxDeaths, TotalDeaths[tank.uid])
120 }
121 ngames++
122 }
123
124 let tbody = this.stats.querySelector("tbody")
125 tbody.replaceChildren()
126
127 let byKills = Object.keys(TankNames)
128 byKills.sort((a, b) => TotalKills[a] - TotalKills[b])
129 byKills.reverse()
130
131 for (let uid of byKills) {
132 let tr = tbody.appendChild(document.createElement("tr"))
133
134 let tdSwatch = tr.appendChild(document.createElement("td"))
135 tdSwatch.class = "swatch"
136 tdSwatch.style.backgroundColor = TankColors[uid]
137 tdSwatch.textContent = "#"
138
139 tr.appendChild(document.createElement("td")).textContent = TankNames[uid]
140 tr.appendChild(document.createElement("td")).textContent = TotalGames[uid]
141 tr.appendChild(document.createElement("td")).textContent = TotalKills[uid]
142 tr.appendChild(document.createElement("td")).textContent = TotalDeaths[uid]
143
144 let award = []
145 if (TotalGames[uid] < 20) {
146 award.push("noob")
147 }
148 if (TotalKills[uid] == MaxKills) {
149 award.push("ruthless")
150 }
151 if (TotalDeaths[uid] == MaxDeaths) {
152 award.push("punished")
153 }
154 if (TotalDeaths[uid] == 0) {
155 award.push("invincible")
156 }
157 if (TotalDeaths[uid] < ngames*0.33) {
158 award.push("stealthy")
159 }
160 if (TotalDeaths[uid] == ngames) {
161 award.push("wasted")
162 }
163 if (TotalGames[uid] > ngames) {
164 award.push("overachiever")
165 }
166 if (TotalKills[uid] == TotalDeaths[uid]) {
167 award.push("balanced")
168 }
169 tr.appendChild(document.createElement("td")).textContent = award.join(", ")
170 }
171 }
172
173 async reloadPlayer() {
174 let fn = this.select.value
175 if (fn == "latest") {
176 let fns = this.fns()
177 fn = fns[fns.length - 1]
178 }
179 let game = this.games[fn]
180
181 this.player.load(game)
182
183 this.dateOutput.value = dateOfFn(fn)
184
185 let tbody = this.results.querySelector("tbody")
186 tbody.replaceChildren()
187
188 let byKills = Object.keys(game.tanks)
189 byKills.sort((a, b) => game.tanks[a].kills - game.tanks[b].kills)
190 byKills.reverse()
191
192 for (let i of byKills) {
193 let tank = game.tanks[i]
194 let tr = tbody.appendChild(document.createElement("tr"))
195
196 let tdSwatch = tr.appendChild(document.createElement("td"))
197 tdSwatch.class = "swatch"
198 tdSwatch.style.backgroundColor = tank.color
199 tdSwatch.textContent = "#"
200
201 let tdName = tr.appendChild(document.createElement("td"))
202 tdName.textContent = tank.name
203
204 tr.appendChild(document.createElement("td")).textContent = tank.kills
205 tr.appendChild(document.createElement("td")).textContent = tank.death
206 tr.appendChild(document.createElement("td")).textContent = game.tanks[tank.killer]?.name
207 tr.appendChild(document.createElement("td")).textContent = `${tank.error} @${tank.errorPos}`
208 }
209 }
210}
211
212
213export function init() {
214 let replay = new Replay(
215 document.querySelector("canvas#battlefield").getContext("2d"),
216 document.querySelector("select#game"),
217 document.querySelector("table#results"),
218 document.querySelector("output#date"),
219 document.querySelector("table#stats"),
220 )
221 replay.refresh()
222 setInterval(() => replay.refresh(), Minute / 5)
223}