@v12-engine/create 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +4 -1
  2. package/base/_gitignore +5 -0
  3. package/base/package.json +3 -3
  4. package/base/src/main.ts +36 -10
  5. package/base/src/navigation.worker.ts +7 -0
  6. package/base/vite.config.ts +24 -2
  7. package/cli.mjs +21 -6
  8. package/index.mjs +27 -7
  9. package/layers/desktop/electron/app.mjs +237 -0
  10. package/layers/desktop/electron/config.mjs +16 -0
  11. package/layers/desktop/electron/dev.mjs +63 -0
  12. package/layers/desktop/electron/files.mjs +86 -0
  13. package/layers/desktop/electron/icons/icon.icns +0 -0
  14. package/layers/desktop/electron/icons/icon.png +0 -0
  15. package/layers/desktop/electron/main.mjs +12 -0
  16. package/layers/desktop/electron/package.mjs +75 -0
  17. package/layers/desktop/electron/preload.cjs +41 -0
  18. package/layers/desktop/electron/presentation.m +87 -0
  19. package/layers/desktop/electron/serve.mjs +76 -0
  20. package/layers/desktop/electron/tools.mjs +63 -0
  21. package/layers/desktop/electron/updates.mjs +71 -0
  22. package/layers/desktop/package.json +13 -0
  23. package/layers/desktop/src/desktop.ts +175 -0
  24. package/layers/desktop/src/shell.ts +279 -0
  25. package/layers/desktop/tests/main.test.ts +189 -0
  26. package/layers/desktop/tests/shell.test.ts +152 -0
  27. package/layers/rooms/server.ts +13 -10
  28. package/layers/server/server.ts +4 -5
  29. package/layers/source/vite.config.ts +21 -1
  30. package/package.json +1 -1
  31. package/starters/campaign/README.md +9 -2
  32. package/starters/campaign/layers/rooms/server.ts +3 -3
  33. package/starters/campaign/layers/server/server.ts +3 -5
  34. package/starters/campaign/layers/tests/tests/rules.test.ts +40 -21
  35. package/starters/campaign/src/campaign/game.ts +26 -5
  36. package/starters/campaign/src/campaign/index.ts +104 -68
  37. package/starters/campaign/src/campaign/kills.ts +1 -1
  38. package/starters/campaign/src/campaign/missions/homecoming.ts +7 -7
  39. package/starters/campaign/src/campaign/missions/siege.ts +6 -6
  40. package/starters/campaign/src/campaign/missions/warlord.ts +13 -14
  41. package/starters/campaign/src/campaign/scenes.ts +8 -7
  42. package/starters/campaign/src/campaign/units.ts +59 -20
  43. package/starters/campaign/src/campaign/view/attacks.ts +19 -0
  44. package/starters/campaign/src/campaign/view/fire.ts +2 -2
  45. package/starters/campaign/src/campaign/view/index.ts +54 -43
  46. package/starters/campaign/src/campaign/waves.ts +18 -17
  47. package/starters/campaign/src/game.ts +8 -1
  48. package/starters/campaign/src/main.ts +38 -19
  49. package/starters/campaign/src/maps/fort.names.ts +2 -2
  50. package/starters/campaign/src/maps/fort.vgmap.json +102 -2
  51. package/starters/campaign/src/maps/homestead.names.ts +2 -2
  52. package/starters/campaign/src/maps/homestead.vgmap.json +85 -1
  53. package/starters/campaign/src/maps/pass.names.ts +2 -2
  54. package/starters/campaign/src/maps/pass.vgmap.json +69 -1
  55. package/starters/campaign/src/waves-panel.ts +2 -1
  56. package/starters/empty/README.md +18 -10
  57. package/starters/empty/layers/tests/tests/rules.test.ts +16 -17
  58. package/starters/empty/src/game.ts +11 -75
  59. package/starters/empty/src/generate.worker.ts +5 -5
  60. package/starters/empty/src/rules.ts +83 -25
  61. package/starters/empty/src/view.ts +82 -0
  62. package/starters/examples/README.md +83 -0
  63. package/starters/examples/layers/tests/tests/examples.test.ts +30 -0
  64. package/starters/examples/package.json +5 -0
  65. package/starters/examples/src/examples/abilities/areas.ts +165 -0
  66. package/starters/examples/src/examples/abilities/areasLooks.ts +30 -0
  67. package/starters/examples/src/examples/abilities/buffs.ts +287 -0
  68. package/starters/examples/src/examples/abilities/buffsLooks.ts +46 -0
  69. package/starters/examples/src/examples/abilities/changing.ts +228 -0
  70. package/starters/examples/src/examples/abilities/changingLooks.ts +61 -0
  71. package/starters/examples/src/examples/abilities/control.ts +158 -0
  72. package/starters/examples/src/examples/abilities/controlLooks.ts +78 -0
  73. package/starters/examples/src/examples/abilities/damage.ts +156 -0
  74. package/starters/examples/src/examples/abilities/damageLooks.ts +37 -0
  75. package/starters/examples/src/examples/abilities/family.ts +29 -0
  76. package/starters/examples/src/examples/abilities/index.ts +32 -0
  77. package/starters/examples/src/examples/abilities/kit.ts +179 -0
  78. package/starters/examples/src/examples/abilities/lookKit.ts +71 -0
  79. package/starters/examples/src/examples/abilities/looks.ts +47 -0
  80. package/starters/examples/src/examples/abilities/missiles.ts +203 -0
  81. package/starters/examples/src/examples/abilities/missilesLooks.ts +38 -0
  82. package/starters/examples/src/examples/abilities/moves.ts +206 -0
  83. package/starters/examples/src/examples/abilities/movesLooks.ts +52 -0
  84. package/starters/examples/src/examples/abilities/recasts.ts +181 -0
  85. package/starters/examples/src/examples/abilities/recastsLooks.ts +41 -0
  86. package/starters/examples/src/examples/abilities/resources.ts +166 -0
  87. package/starters/examples/src/examples/abilities/resourcesLooks.ts +42 -0
  88. package/starters/examples/src/examples/abilities/summons.ts +296 -0
  89. package/starters/examples/src/examples/abilities/summonsLooks.ts +115 -0
  90. package/starters/examples/src/examples/abilities/triggers.ts +204 -0
  91. package/starters/examples/src/examples/abilities/triggersLooks.ts +53 -0
  92. package/starters/examples/src/examples/abilities/vision.ts +137 -0
  93. package/starters/examples/src/examples/abilities/visionLooks.ts +50 -0
  94. package/starters/examples/src/examples/animation/index.ts +235 -0
  95. package/starters/examples/src/examples/animation/looks.ts +444 -0
  96. package/starters/examples/src/examples/catalog.ts +52 -0
  97. package/starters/examples/src/examples/cinematics/index.ts +217 -0
  98. package/starters/examples/src/examples/cinematics/looks.ts +476 -0
  99. package/starters/examples/src/examples/cinematics/tour.json +43 -0
  100. package/starters/examples/src/examples/cinematics/village.ts +88 -0
  101. package/starters/examples/src/examples/common/looks.ts +65 -0
  102. package/starters/examples/src/examples/common/units.ts +70 -0
  103. package/starters/examples/src/examples/construction/index.ts +329 -0
  104. package/starters/examples/src/examples/construction/looks.ts +275 -0
  105. package/starters/examples/src/examples/custom-animation/bones.ts +92 -0
  106. package/starters/examples/src/examples/custom-animation/index.ts +190 -0
  107. package/starters/examples/src/examples/custom-animation/looks.ts +523 -0
  108. package/starters/examples/src/examples/custom-animation/runes.ts +82 -0
  109. package/starters/examples/src/examples/custom-fx/glaive.look.ts +110 -0
  110. package/starters/examples/src/examples/custom-fx/glaive.ts +40 -0
  111. package/starters/examples/src/examples/custom-fx/gpu.look.ts +145 -0
  112. package/starters/examples/src/examples/custom-fx/gpu.ts +22 -0
  113. package/starters/examples/src/examples/custom-fx/index.ts +25 -0
  114. package/starters/examples/src/examples/custom-fx/lights.look.ts +144 -0
  115. package/starters/examples/src/examples/custom-fx/lights.ts +25 -0
  116. package/starters/examples/src/examples/custom-fx/looks.ts +36 -0
  117. package/starters/examples/src/examples/custom-fx/meteor.look.ts +133 -0
  118. package/starters/examples/src/examples/custom-fx/meteor.ts +26 -0
  119. package/starters/examples/src/examples/custom-fx/parts.look.ts +193 -0
  120. package/starters/examples/src/examples/custom-fx/parts.ts +21 -0
  121. package/starters/examples/src/examples/custom-fx/portal.look.ts +193 -0
  122. package/starters/examples/src/examples/custom-fx/portal.ts +48 -0
  123. package/starters/examples/src/examples/custom-fx/renderer.look.ts +139 -0
  124. package/starters/examples/src/examples/custom-fx/renderer.ts +18 -0
  125. package/starters/examples/src/examples/custom-fx/ribbons.look.ts +151 -0
  126. package/starters/examples/src/examples/custom-fx/ribbons.ts +22 -0
  127. package/starters/examples/src/examples/custom-fx/shader.ts +42 -0
  128. package/starters/examples/src/examples/custom-fx/shield.look.ts +226 -0
  129. package/starters/examples/src/examples/custom-fx/shield.ts +43 -0
  130. package/starters/examples/src/examples/define.ts +86 -0
  131. package/starters/examples/src/examples/destruction/index.ts +570 -0
  132. package/starters/examples/src/examples/destruction/looks.ts +447 -0
  133. package/starters/examples/src/examples/dialogs/index.ts +383 -0
  134. package/starters/examples/src/examples/dialogs/looks.ts +546 -0
  135. package/starters/examples/src/examples/dynamic-map/index.ts +603 -0
  136. package/starters/examples/src/examples/dynamic-map/looks.ts +301 -0
  137. package/starters/examples/src/examples/fx/blood.look.ts +62 -0
  138. package/starters/examples/src/examples/fx/blood.ts +59 -0
  139. package/starters/examples/src/examples/fx/feel.look.ts +86 -0
  140. package/starters/examples/src/examples/fx/feel.ts +57 -0
  141. package/starters/examples/src/examples/fx/fight.ts +28 -0
  142. package/starters/examples/src/examples/fx/floats.look.ts +108 -0
  143. package/starters/examples/src/examples/fx/floats.ts +24 -0
  144. package/starters/examples/src/examples/fx/gallery.look.ts +168 -0
  145. package/starters/examples/src/examples/fx/gallery.ts +23 -0
  146. package/starters/examples/src/examples/fx/index.ts +30 -0
  147. package/starters/examples/src/examples/fx/links.look.ts +83 -0
  148. package/starters/examples/src/examples/fx/links.ts +49 -0
  149. package/starters/examples/src/examples/fx/looks.ts +55 -0
  150. package/starters/examples/src/examples/fx/map.look.ts +93 -0
  151. package/starters/examples/src/examples/fx/map.ts +33 -0
  152. package/starters/examples/src/examples/fx/marks.look.ts +100 -0
  153. package/starters/examples/src/examples/fx/marks.ts +36 -0
  154. package/starters/examples/src/examples/fx/shots.look.ts +92 -0
  155. package/starters/examples/src/examples/fx/shots.ts +71 -0
  156. package/starters/examples/src/examples/fx/spells.look.ts +87 -0
  157. package/starters/examples/src/examples/fx/spells.ts +122 -0
  158. package/starters/examples/src/examples/fx/stage.ts +31 -0
  159. package/starters/examples/src/examples/gui/babylonScreen.ts +167 -0
  160. package/starters/examples/src/examples/gui/babylonWorld.ts +191 -0
  161. package/starters/examples/src/examples/gui/htmlPart.ts +95 -0
  162. package/starters/examples/src/examples/gui/index.ts +143 -0
  163. package/starters/examples/src/examples/gui/keys.ts +91 -0
  164. package/starters/examples/src/examples/gui/layout.ts +42 -0
  165. package/starters/examples/src/examples/gui/looks.ts +270 -0
  166. package/starters/examples/src/examples/gui/minimap.ts +127 -0
  167. package/starters/examples/src/examples/gui/ownInterface.ts +129 -0
  168. package/starters/examples/src/examples/gui/plates.ts +87 -0
  169. package/starters/examples/src/examples/gui/radial.ts +104 -0
  170. package/starters/examples/src/examples/gui/solidPart.ts +107 -0
  171. package/starters/examples/src/examples/looks.ts +45 -0
  172. package/starters/examples/src/examples/mechanics/battle.ts +51 -0
  173. package/starters/examples/src/examples/mechanics/damage.ts +80 -0
  174. package/starters/examples/src/examples/mechanics/flying.ts +93 -0
  175. package/starters/examples/src/examples/mechanics/formations.ts +58 -0
  176. package/starters/examples/src/examples/mechanics/highGround.ts +82 -0
  177. package/starters/examples/src/examples/mechanics/index.ts +44 -0
  178. package/starters/examples/src/examples/mechanics/looks.ts +289 -0
  179. package/starters/examples/src/examples/mechanics/maze.ts +119 -0
  180. package/starters/examples/src/examples/mechanics/orders.ts +68 -0
  181. package/starters/examples/src/examples/mechanics/selection.ts +30 -0
  182. package/starters/examples/src/examples/moba/index.ts +377 -0
  183. package/starters/examples/src/examples/moba/looks.ts +349 -0
  184. package/starters/examples/src/examples/moba/maps.ts +69 -0
  185. package/starters/examples/src/examples/moba/spells.ts +34 -0
  186. package/starters/examples/src/examples/moba/units.ts +196 -0
  187. package/starters/examples/src/examples/nature/camera.ts +15 -0
  188. package/starters/examples/src/examples/nature/grass.ts +64 -0
  189. package/starters/examples/src/examples/nature/index.ts +327 -0
  190. package/starters/examples/src/examples/nature/land.ts +173 -0
  191. package/starters/examples/src/examples/nature/looks.ts +577 -0
  192. package/starters/examples/src/examples/nature/maps.ts +370 -0
  193. package/starters/examples/src/examples/nature/skies.ts +324 -0
  194. package/starters/examples/src/examples/nature/weather.ts +80 -0
  195. package/starters/examples/src/examples/performance/index.ts +263 -0
  196. package/starters/examples/src/examples/performance/looks.ts +372 -0
  197. package/starters/examples/src/examples/performance/meter.ts +158 -0
  198. package/starters/examples/src/examples/post/grades.ts +86 -0
  199. package/starters/examples/src/examples/post/index.ts +202 -0
  200. package/starters/examples/src/examples/post/looks.ts +884 -0
  201. package/starters/examples/src/examples/post/passes.ts +213 -0
  202. package/starters/examples/src/examples/rts/ai.ts +49 -0
  203. package/starters/examples/src/examples/rts/airlift.ts +43 -0
  204. package/starters/examples/src/examples/rts/base.ts +129 -0
  205. package/starters/examples/src/examples/rts/build.ts +65 -0
  206. package/starters/examples/src/examples/rts/camps.ts +36 -0
  207. package/starters/examples/src/examples/rts/carry.ts +103 -0
  208. package/starters/examples/src/examples/rts/creeps.ts +61 -0
  209. package/starters/examples/src/examples/rts/data.ts +406 -0
  210. package/starters/examples/src/examples/rts/ferry.ts +41 -0
  211. package/starters/examples/src/examples/rts/garrison.ts +50 -0
  212. package/starters/examples/src/examples/rts/harvest.ts +29 -0
  213. package/starters/examples/src/examples/rts/hero.ts +67 -0
  214. package/starters/examples/src/examples/rts/index.ts +77 -0
  215. package/starters/examples/src/examples/rts/looks.ts +721 -0
  216. package/starters/examples/src/examples/rts/shop.ts +46 -0
  217. package/starters/examples/src/examples/rts/tech.ts +39 -0
  218. package/starters/examples/src/examples/rts/train.ts +48 -0
  219. package/starters/examples/src/examples/rts/waterside.ts +185 -0
  220. package/starters/examples/src/examples/showcase/index.ts +714 -0
  221. package/starters/examples/src/examples/showcase/looks.ts +238 -0
  222. package/starters/examples/src/game.ts +17 -0
  223. package/starters/examples/src/main.ts +82 -0
  224. package/starters/examples/src/rules.ts +31 -0
  225. package/starters/examples/src/shell/look.ts +79 -0
  226. package/starters/examples/src/shell/registry.ts +7 -0
  227. package/starters/examples/src/shell/scenes.ts +178 -0
  228. package/starters/examples/src/shell/shell.ts +290 -0
  229. package/starters/examples/src/shell/style.ts +202 -0
  230. package/starters/examples/src/view.ts +50 -0
  231. package/starters/melee/README.md +12 -3
  232. package/starters/melee/layers/tests/tests/heroes.test.ts +402 -0
  233. package/starters/melee/layers/tests/tests/rules.test.ts +10 -8
  234. package/starters/melee/src/game.ts +32 -2
  235. package/starters/melee/src/maps/bay.names.ts +2 -2
  236. package/starters/melee/src/maps/bay.vgmap.json +198 -1
  237. package/starters/melee/src/maps.ts +11 -9
  238. package/starters/melee/src/melee/abilities.ts +1 -194
  239. package/starters/melee/src/melee/blight.ts +7 -7
  240. package/starters/melee/src/melee/cellLayers.json +3 -3
  241. package/starters/melee/src/melee/data.json +1 -18
  242. package/starters/melee/src/melee/game.ts +6 -4
  243. package/starters/melee/src/melee/heroes.ts +1025 -0
  244. package/starters/melee/src/melee/index.ts +64 -61
  245. package/starters/melee/src/melee/items.ts +15 -362
  246. package/starters/melee/src/melee/languages/he.json +14 -2
  247. package/starters/melee/src/melee/pings.ts +5 -6
  248. package/starters/melee/src/melee/saves.ts +1 -1
  249. package/starters/melee/src/melee/undead.ts +15 -230
  250. package/starters/melee/src/melee/units.ts +37 -171
  251. package/starters/melee/src/melee/view/attacks.ts +19 -0
  252. package/starters/melee/src/melee/view/heroSpellLooks.ts +39 -8
  253. package/starters/melee/src/melee/view/index.ts +56 -52
  254. package/starters/melee/src/melee/view/looks.ts +86 -63
  255. package/starters/melee/src/melee/view/undeadLooks.ts +70 -31
  256. package/starters/melee/src/melee/view/walls.ts +1 -1
  257. package/starters.mjs +6 -0
  258. package/v12.mjs +14 -15
  259. package/starters/campaign/src/rules.ts +0 -20
  260. package/starters/empty/src/maps.ts +0 -44
  261. package/starters/empty/src/tiles.ts +0 -12
  262. package/starters/melee/src/melee/heroSpells.ts +0 -38
  263. package/starters/melee/src/rules.ts +0 -24
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Keys of the game's own: actions with a name, a label, a group and default keys (`ActionDef`), added to the client's
3
+ * while the game runs (`client.keys.define`; a game says them once in its view: `input.keys.actions.add`). The player
4
+ * can rebind them on the Keys screen of the pause menu as any of the client's; here a button rebinds one in code
5
+ * (`client.keys.bind`). A legend of the HUD's (`hud.widgets`) lists them with the keys they are on now, and follows a
6
+ * rebinding (`client.keys.onChange`)
7
+ */
8
+ import type { GameClientApi, HudWidgetDef } from '@v12-engine/client'
9
+ import { PLAYER } from '../common/units'
10
+ import { gather } from './index'
11
+
12
+ /** The example's actions, by id: what each does, and the keys it comes with */
13
+ export const ACTIONS = [
14
+ {
15
+ id: 'gui.gather',
16
+ label: 'Gather the army where the camera looks',
17
+ default: ['KeyR'],
18
+ run: (client: GameClientApi) => {
19
+ const { x, z } = client.camera.pose()
20
+ client.session.send(gather.make({ at: { x, z } }))
21
+ client.ping(x, z, [0.4, 0.8, 1])
22
+ }
23
+ },
24
+ {
25
+ id: 'gui.cheer',
26
+ label: 'Everyone jumps for joy',
27
+ default: ['KeyC'],
28
+ run: (client: GameClientApi) => {
29
+ for (const unit of client.session.snapshot().entities) if (unit.owner === PLAYER && unit.health > 0) client.playClip(unit.id, 'Jump_Full_Short')
30
+ }
31
+ },
32
+ {
33
+ id: 'gui.hero',
34
+ label: 'Select the knight and look at him',
35
+ default: ['KeyZ'],
36
+ run: (client: GameClientApi) => {
37
+ const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
38
+ if (!hero) return
39
+ client.controller.select([hero.id])
40
+ client.camera.lookAt({ x: hero.x, z: hero.z }, { seconds: 0.5 })
41
+ }
42
+ }
43
+ ] as const
44
+
45
+ /** Adds the actions to the client's keys; gives back what takes them away */
46
+ export function defineKeys(client: GameClientApi): () => void {
47
+ for (const action of ACTIONS)
48
+ client.keys.define({ id: action.id, label: action.label, group: 'Examples', default: action.default, run: action.run })
49
+ return () => {
50
+ for (const action of ACTIONS) client.keys.forget(action.id)
51
+ }
52
+ }
53
+
54
+ /** The legend: each action with its keys as the player's keyboard shows them, drawn again when a binding changes */
55
+ export function keysLegend(client: GameClientApi): HudWidgetDef {
56
+ return {
57
+ id: 'gui.keys',
58
+ place: 'left',
59
+ mount(element) {
60
+ const panel = document.createElement('div')
61
+ panel.style.cssText =
62
+ 'pointer-events:auto;width:270px;padding:12px 14px;border-radius:8px;background:rgba(12,14,20,.88);border:1px solid #3c4252;color:#e8e2d0;font:13px/1.35 system-ui,sans-serif'
63
+ element.append(panel)
64
+ const draw = () => {
65
+ panel.innerHTML = '<div style="font:600 15px system-ui;color:#f2d58a;margin-bottom:8px">Your keys</div>'
66
+ for (const action of ACTIONS) {
67
+ const row = document.createElement('div')
68
+ row.style.cssText = 'display:flex;gap:10px;align-items:center;margin:6px 0'
69
+ const key = document.createElement('kbd')
70
+ key.textContent = client.keys.keys(action.id).join(' / ') || '—'
71
+ key.style.cssText =
72
+ 'min-width:28px;text-align:center;padding:4px 7px;border-radius:5px;background:linear-gradient(#3a3f4c,#1d2028);border:1px solid #5a6070;box-shadow:0 2px 0 #0a0b0e;font:700 13px system-ui;color:#ffe9a8'
73
+ const words = document.createElement('span')
74
+ words.textContent = action.label
75
+ row.append(key, words)
76
+ panel.append(row)
77
+ }
78
+ const note = document.createElement('div')
79
+ note.style.cssText = 'margin-top:8px;color:#8a8f9c;font-size:11px'
80
+ note.textContent = 'Rebind them on the Keys screen of the menu (F10), or with the buttons on the card'
81
+ panel.append(note)
82
+ }
83
+ draw()
84
+ const stop = client.keys.onChange(draw)
85
+ return () => {
86
+ stop()
87
+ panel.remove()
88
+ }
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The HUD's own parts, changed while the game runs: the console laid out centred or in the corners
3
+ * (`client.hud.setConsole`), a theme by name (`client.theme.set`: the colours, the frames' metal, the typefaces),
4
+ * parts hidden and shown (`client.hud.hide`), the HUD's size (`client.hud.setScale`), the top bar's title
5
+ * (`client.hud.setTitle`), the minimap's size (`client.hud.setMinimap`), or the whole HUD hidden (`client.hideHud`).
6
+ * Each is put back as the example is left
7
+ */
8
+ import type { GameClientApi } from '@v12-engine/client'
9
+
10
+ /** The themes to pick from: the client's own (`themeNames()` lists every one, the editor's too) */
11
+ export const THEMES = ['classic', 'stone', 'moonlight', 'plain', 'iron', 'bone', 'jade'] as const
12
+
13
+ /**
14
+ * The parts the toggles hide, by the client's ids (`HUD_WIDGETS`), with their words: those this game shows. The top
15
+ * bar's clock and resources, and the messages, are hidden the same way (`'clock'`, `'resources'`, `'messages'`): this
16
+ * game keeps no resources nor time, and says nothing to show
17
+ */
18
+ export const PARTS = [
19
+ ['minimap', 'Minimap'],
20
+ ['card', 'Command card'],
21
+ ['selection', 'Selection']
22
+ ] as const
23
+
24
+ /** What the example changed, to put back as it is left: the whole HUD hidden */
25
+ const changed: { shown?: () => void } = {}
26
+
27
+ /** Hides the whole HUD, or shows it again (`client.hideHud` gives back what shows it) */
28
+ export function hideAll(client: GameClientApi, hidden: boolean): void {
29
+ changed.shown?.()
30
+ changed.shown = hidden ? client.hideHud() : undefined
31
+ }
32
+
33
+ /** Puts back everything the example changed: the HUD as the game's view says it */
34
+ export function restoreHud(client: GameClientApi): void {
35
+ hideAll(client, false)
36
+ client.hud.setMinimap({ size: 212 })
37
+ client.hud.setConsole('centered')
38
+ client.theme.reset()
39
+ for (const [id] of PARTS) client.hud.hide(id, false)
40
+ client.hud.setScale(1)
41
+ client.hud.setTitle(undefined)
42
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * The interface examples on the client's side: their controls, and what each puts up as it is opened (`ready`) and
3
+ * takes down as it is left. Each interface is a file of its own beside this one: Babylon's GUI on the screen
4
+ * (`babylonScreen.ts`) and in the world (`babylonWorld.ts`), a HUD part in Solid (`solidPart.ts`) and one in plain HTML
5
+ * (`htmlPart.ts`), the HUD's own parts rearranged (`layout.ts`), plates over units (`plates.ts`), a minimap
6
+ * (`minimap.ts`), a radial menu (`radial.ts`), a whole interface of the game's own (`ownInterface.ts`) and keys
7
+ * (`keys.ts`)
8
+ */
9
+ import type { GameClientApi } from '@v12-engine/client'
10
+ import { button, type CategoryLooks, select, slider, toggle } from '../../shell/look'
11
+ import { aim } from '../cinematics/looks'
12
+ import { babylonPanel } from './babylonScreen'
13
+ import { babylonWorld, POST } from './babylonWorld'
14
+ import { rosterPart } from './htmlPart'
15
+ import { charge, gather, resetCamp } from './index'
16
+ import { ACTIONS, defineKeys, keysLegend } from './keys'
17
+ import { hideAll, PARTS, restoreHud, THEMES } from './layout'
18
+ import { minimapLayer, ownMinimap } from './minimap'
19
+ import { ownInterface } from './ownInterface'
20
+ import { DESIGNS, restorePlates, setDesign } from './plates'
21
+ import { closeRadial, radialRightClick } from './radial'
22
+ import { armyPart } from './solidPart'
23
+
24
+ /** The camp from the south, both sides in the picture */
25
+ const overTheCamp = (client: GameClientApi, distance = 30) => aim(client, { x: 24, z: 23 }, distance)
26
+
27
+ /** The two buttons every camp has: the battle, and the camp laid out again */
28
+ const battle = [
29
+ button(
30
+ 'Charge',
31
+ (client) => client.session.send(charge.make({})),
32
+ 'Sends both camps at each other: watch the interface follow the fight, health falling and units dying.'
33
+ ),
34
+ button(
35
+ 'Lay it out again',
36
+ (client) => client.session.send(resetCamp.make({})),
37
+ 'Puts both camps back as they started, everyone whole, for another charge.'
38
+ )
39
+ ]
40
+
41
+ /** What the minimap example has up now: the game's own minimap, or a layer on the client's */
42
+ const minimapNow: { off?: () => void } = {}
43
+
44
+ /** The interface of the game's own while it is up: what takes it down */
45
+ const own: { off?: () => void } = {}
46
+
47
+ export const category: CategoryLooks = {
48
+ looks: {
49
+ 'gui-babylon-screen': {
50
+ controls: battle,
51
+ ready: (client) => {
52
+ overTheCamp(client)
53
+ return babylonPanel(client)
54
+ }
55
+ },
56
+ 'gui-babylon-world': {
57
+ controls: [
58
+ ...battle,
59
+ button(
60
+ 'Gather at the sign',
61
+ (client) => client.session.send(gather.make({ at: { x: POST.x - 2, z: POST.z - 2 } })),
62
+ 'Sends your army to the signpost: the same as clicking the button on the sign itself, which is Babylon GUI drawn in the world.'
63
+ )
64
+ ],
65
+ ready: (client) => {
66
+ overTheCamp(client, 28)
67
+ return babylonWorld(client)
68
+ }
69
+ },
70
+ 'gui-solid': {
71
+ controls: battle,
72
+ ready: (client) => {
73
+ overTheCamp(client)
74
+ return client.hud.widgets.add(armyPart(client))
75
+ }
76
+ },
77
+ 'gui-html': {
78
+ controls: battle,
79
+ ready: (client) => {
80
+ overTheCamp(client)
81
+ return client.hud.widgets.add(rosterPart(client))
82
+ }
83
+ },
84
+ 'gui-hud-layout': {
85
+ controls: [
86
+ select(
87
+ 'Console',
88
+ ['centered', 'corners'],
89
+ 'centered',
90
+ (client, layout) => client.hud.setConsole(layout as 'centered' | 'corners'),
91
+ 'How the bottom console is laid out: one centred bar, or the minimap and the card apart in the corners.'
92
+ ),
93
+ select(
94
+ 'Theme',
95
+ THEMES,
96
+ 'classic',
97
+ (client, theme) => client.theme.set(theme),
98
+ 'The HUD’s theme: its colours, the metal of its frames and its typefaces change at once.'
99
+ ),
100
+ slider(
101
+ 'Size',
102
+ { min: 0.7, max: 1.3, step: 0.05, value: 1 },
103
+ (client, size) => client.hud.setScale(size),
104
+ 'How big the whole HUD is drawn, from 70% to 130%.'
105
+ ),
106
+ slider(
107
+ 'Minimap size',
108
+ { min: 150, max: 320, step: 10, value: 212 },
109
+ (client, size) => client.hud.setMinimap({ size }),
110
+ 'How big the minimap is, in pixels: the console makes room for it.'
111
+ ),
112
+ ...PARTS.map(([id, words]) =>
113
+ toggle(
114
+ words,
115
+ true,
116
+ (client, on) => client.hud.hide(id, !on),
117
+ `Shows or hides this one part of the HUD, the ${words.toLowerCase()}: the rest stays where it is.`
118
+ )
119
+ ),
120
+ button('Title: “The Camp”', (client) => client.hud.setTitle('The Camp'), 'Sets the title the top bar shows to “The Camp”.'),
121
+ toggle(
122
+ 'Hide the whole HUD',
123
+ false,
124
+ (client, on) => hideAll(client, on),
125
+ 'Hides the whole HUD at once, for a clean picture (client.hideHud); off, it comes back as it was.'
126
+ ),
127
+ button('Put it all back', (client) => restoreHud(client), 'Undoes every change above: the HUD as the game’s view says it.')
128
+ ],
129
+ ready: (client) => {
130
+ overTheCamp(client)
131
+ // the knight selected: the selection's panel has him to show
132
+ const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
133
+ if (hero) client.controller.select([hero.id])
134
+ return () => restoreHud(client)
135
+ }
136
+ },
137
+ 'gui-plates': {
138
+ controls: [
139
+ select(
140
+ 'Design',
141
+ Object.keys(DESIGNS),
142
+ 'Hero card',
143
+ (client, design) => setDesign(client, design),
144
+ 'What the plate over each unit shows: a thin bar only while hurt, a name over a bar, a hero’s card… Charge to see the bars fall.'
145
+ ),
146
+ select(
147
+ 'Drawn as',
148
+ ['canvas', 'dom', 'gui'],
149
+ 'canvas',
150
+ (client, render) => (client.unitHud.look.render = render as 'canvas' | 'dom' | 'gui'),
151
+ 'What draws the plates: one canvas over the picture, an HTML element each, or Babylon GUI. They look the same; what it costs differs.'
152
+ ),
153
+ toggle(
154
+ 'Bars over everyone',
155
+ false,
156
+ (client, on) => (client.showAllBars = on),
157
+ 'Shows the plates over every unit, not only the selected, hovered or hurt, as holding Alt does: plain to see with the Bars when hurt design, which shows none till then.'
158
+ ),
159
+ ...battle
160
+ ],
161
+ ready: (client) => {
162
+ overTheCamp(client, 24)
163
+ setDesign(client, 'Hero card')
164
+ return () => restorePlates(client)
165
+ }
166
+ },
167
+ 'gui-minimap': {
168
+ controls: [
169
+ select(
170
+ 'Minimap',
171
+ ['ours', 'the client’s with a layer', 'the client’s'],
172
+ 'ours',
173
+ (client, which) => {
174
+ minimapNow.off?.()
175
+ minimapNow.off = which === 'ours' ? ownMinimap(client) : which === 'the client’s' ? undefined : minimapLayer(client)
176
+ },
177
+ 'Which minimap shows, bottom left: one the game draws itself, the client’s with the camps and a ring round the knight drawn on it, or the client’s as it is.'
178
+ ),
179
+ ...battle
180
+ ],
181
+ ready: (client) => {
182
+ overTheCamp(client)
183
+ minimapNow.off = ownMinimap(client)
184
+ return () => {
185
+ minimapNow.off?.()
186
+ minimapNow.off = undefined
187
+ }
188
+ }
189
+ },
190
+ 'gui-radial': {
191
+ view: (view) => ({ ...view, input: { ...view.input, onRightClick: radialRightClick } }),
192
+ controls: [
193
+ button(
194
+ 'Select your army',
195
+ (client) =>
196
+ client.controller.select(
197
+ client.session
198
+ .snapshot()
199
+ .entities.filter((each) => each.owner === client.session.playerId)
200
+ .map((each) => each.id)
201
+ ),
202
+ 'Selects all your units: then right-click the ground to open the radial menu of orders round the pointer.'
203
+ ),
204
+ ...battle
205
+ ],
206
+ ready: (client) => {
207
+ overTheCamp(client)
208
+ client.controller.select(
209
+ client.session
210
+ .snapshot()
211
+ .entities.filter((each) => each.owner === client.session.playerId)
212
+ .map((each) => each.id)
213
+ )
214
+ return () => closeRadial()
215
+ }
216
+ },
217
+ 'gui-own-interface': {
218
+ controls: [
219
+ toggle(
220
+ 'Ours in place of the HUD',
221
+ true,
222
+ (client, on) => {
223
+ own.off?.()
224
+ own.off = on ? ownInterface(client) : undefined
225
+ },
226
+ 'Whether the game’s own interface replaces the client’s HUD: off, the client’s HUD is back.'
227
+ ),
228
+ ...battle
229
+ ],
230
+ ready: (client) => {
231
+ overTheCamp(client)
232
+ own.off = ownInterface(client)
233
+ return () => {
234
+ own.off?.()
235
+ own.off = undefined
236
+ }
237
+ }
238
+ },
239
+ 'gui-keys': {
240
+ controls: [
241
+ ...ACTIONS.map((action) =>
242
+ button(
243
+ action.label,
244
+ (client) => client.keys.press(action.id),
245
+ `Runs this action as pressing its key does (${action.default[0].slice(3)} at first): the legend lists each with its key.`
246
+ )
247
+ ),
248
+ button(
249
+ 'Put Gather on G',
250
+ (client) => client.keys.bind('gui.gather', ['KeyG']),
251
+ 'Moves the Gather action from R to G, in code (client.keys.bind): the legend changes, and G now gathers.'
252
+ ),
253
+ button(
254
+ 'Keys back',
255
+ (client) => ACTIONS.forEach((action) => client.keys.reset(action.id)),
256
+ 'Puts every action back on its first key: the legend follows.'
257
+ )
258
+ ],
259
+ ready: (client) => {
260
+ overTheCamp(client)
261
+ const forget = defineKeys(client)
262
+ const legend = client.hud.widgets.add(keysLegend(client))
263
+ return () => {
264
+ legend()
265
+ forget()
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * A minimap of the game's own, two ways:
3
+ *
4
+ * - In the client's place: the client's hidden (`client.hud.hide('minimap')`) and a widget of the game's own with a
5
+ * `mount`, drawing a canvas of its own every frame (the field, the units as dots in their side's colour, the frame
6
+ * of what the camera sees) and turning a click into a look at that spot
7
+ * - Over the client's: a layer drawn on the client's own minimap (`client.hud.addMinimapLayer`), here the two camps'
8
+ * grounds and a ring pulsing round the knight
9
+ */
10
+ import type { GameClientApi } from '@v12-engine/client'
11
+ import { PLAYER } from '../common/units'
12
+
13
+ /** The field's size in cells: the examples' field (`exampleField`) */
14
+ const SIZE = 48
15
+ /** The minimap's size in pixels */
16
+ const PIXELS = 220
17
+
18
+ /** The minimap of the game's own, in the place of the client's; gives back what takes it away */
19
+ export function ownMinimap(client: GameClientApi): () => void {
20
+ // the client's minimap and its buttons hidden, and the game's own put over the map in the bottom left
21
+ client.hud.hide('minimap')
22
+ const remove = client.hud.widgets.add({
23
+ id: 'gui.minimap',
24
+ place: 'bottom-left',
25
+ mount(element) {
26
+ const canvas = document.createElement('canvas')
27
+ canvas.width = canvas.height = PIXELS * devicePixelRatio
28
+ canvas.style.cssText = `width:${PIXELS}px;height:${PIXELS}px;border-radius:50%;cursor:crosshair;pointer-events:auto;display:block;
29
+ box-shadow:0 0 0 3px #0e1118,0 0 0 5px #c9a35a,0 8px 20px rgba(0,0,0,.6)`
30
+ element.append(canvas)
31
+ const context = canvas.getContext('2d')!
32
+ const scale = canvas.width / SIZE
33
+ // north up: the map's z grows up the minimap
34
+ const toMap = (x: number, z: number) => ({ x: x * scale, y: canvas.height - z * scale })
35
+
36
+ let frame = 0
37
+ const draw = () => {
38
+ frame = requestAnimationFrame(draw)
39
+ const { width, height } = canvas
40
+ context.save()
41
+ context.clearRect(0, 0, width, height)
42
+ // round, like a radar
43
+ context.beginPath()
44
+ context.arc(width / 2, height / 2, width / 2, 0, Math.PI * 2)
45
+ context.clip()
46
+ const ground = context.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, width / 2)
47
+ ground.addColorStop(0, '#3d6b2e')
48
+ ground.addColorStop(1, '#1d3418')
49
+ context.fillStyle = ground
50
+ context.fillRect(0, 0, width, height)
51
+ // a grid every 8 cells
52
+ context.strokeStyle = 'rgba(255,255,255,.07)'
53
+ context.lineWidth = 1
54
+ for (let cell = 8; cell < SIZE; cell += 8) {
55
+ context.beginPath()
56
+ context.moveTo(cell * scale, 0)
57
+ context.lineTo(cell * scale, height)
58
+ context.moveTo(0, cell * scale)
59
+ context.lineTo(width, cell * scale)
60
+ context.stroke()
61
+ }
62
+ // the units: a dot each, bigger for the knights
63
+ for (const unit of client.session.snapshot().entities) {
64
+ if (unit.health <= 0) continue
65
+ const at = toMap(unit.x, unit.z)
66
+ context.fillStyle = unit.owner === PLAYER ? '#5fb0ff' : '#ff5a44'
67
+ context.beginPath()
68
+ context.arc(at.x, at.y, (unit.type === 'knight' ? 4.5 : 2.6) * devicePixelRatio, 0, Math.PI * 2)
69
+ context.fill()
70
+ }
71
+ // what the camera sees, near enough: a box round where it looks, as wide as it is far away
72
+ const pose = client.camera.pose()
73
+ const half = pose.distance * 0.62
74
+ const corner = toMap(pose.x - half, pose.z + half * 0.6)
75
+ context.strokeStyle = '#fff'
76
+ context.lineWidth = 1.5 * devicePixelRatio
77
+ context.strokeRect(corner.x, corner.y, half * 2 * scale, half * 1.2 * scale)
78
+ context.restore()
79
+ }
80
+ draw()
81
+
82
+ // a click: the camera looks there
83
+ canvas.onpointerdown = (event) => {
84
+ const box = canvas.getBoundingClientRect()
85
+ const x = ((event.clientX - box.left) / box.width) * SIZE
86
+ const z = (1 - (event.clientY - box.top) / box.height) * SIZE
87
+ client.camera.lookAt({ x, z }, { seconds: 0.4 })
88
+ }
89
+ return () => {
90
+ cancelAnimationFrame(frame)
91
+ canvas.remove()
92
+ }
93
+ }
94
+ })
95
+ return () => {
96
+ remove()
97
+ client.hud.hide('minimap', false)
98
+ }
99
+ }
100
+
101
+ /** A layer on the client's own minimap: the camps' grounds, and a ring round the knight; gives back what takes it away */
102
+ export function minimapLayer(client: GameClientApi): () => void {
103
+ return client.hud.addMinimapLayer({
104
+ id: 'gui.camps',
105
+ at: 'units',
106
+ draw: (context, frame) => {
107
+ const zone = (x: number, z: number, color: string) => {
108
+ const at = frame.toMap(x, z)
109
+ context.fillStyle = color
110
+ context.beginPath()
111
+ context.ellipse(at.x, at.y, 8 * frame.scale.x, 6 * frame.scale.y, 0, 0, Math.PI * 2)
112
+ context.fill()
113
+ }
114
+ zone(24, 16, 'rgba(80,160,255,.28)')
115
+ zone(24, 32, 'rgba(255,80,60,.28)')
116
+ const hero = frame.client.session.snapshot().entities.find((each) => each.name === 'hero')
117
+ if (!hero) return
118
+ const at = frame.toMap(hero.x, hero.z)
119
+ const pulse = (frame.now / 1000) % 1
120
+ context.strokeStyle = `rgba(255,220,90,${1 - pulse})`
121
+ context.lineWidth = 2
122
+ context.beginPath()
123
+ context.arc(at.x, at.y, (3 + pulse * 6) * frame.scale.x, 0, Math.PI * 2)
124
+ context.stroke()
125
+ }
126
+ })
127
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * An interface of the game's own in place of the whole HUD: the client's hidden (`client.hideHud()`, which gives back
3
+ * what shows it again), and a layer of plain HTML over the world in its place (`client.addLayer`). It has a bar across
4
+ * the top with the battle's numbers, a card for what is selected (from the HUD's state: `client.ui.selection()`), order
5
+ * buttons that send what the client's card would, and a log of the fallen. The selection, orders and camera are the
6
+ * client's as ever: only the drawing is the game's
7
+ */
8
+ import type { GameClientApi } from '@v12-engine/client'
9
+ import { ENEMY, PLAYER } from '../common/units'
10
+ import { charge, gather, resetCamp } from './index'
11
+
12
+ const STYLE = `
13
+ .gui-own { position: absolute; inset: 0; pointer-events: none; font: 14px/1.3 'Trebuchet MS', system-ui, sans-serif; color: #eef3f7 }
14
+ .gui-own .glass { pointer-events: auto; background: linear-gradient(180deg, rgba(20, 34, 48, .92), rgba(8, 14, 22, .92)); border: 1px solid #4fd1c5;
15
+ box-shadow: 0 0 0 1px rgba(0,0,0,.6), 0 0 18px rgba(79, 209, 197, .25); border-radius: 4px }
16
+ .gui-own .top { position: absolute; top: 12px; left: 16px; display: flex; gap: 18px; align-items: center; padding: 8px 16px }
17
+ .gui-own .top b { color: #4fd1c5; letter-spacing: .18em; font-size: 13px }
18
+ .gui-own .top .num { font: 700 20px 'Trebuchet MS', sans-serif }
19
+ .gui-own .top .ours { color: #7cc4ff } .gui-own .top .theirs { color: #ff8b78 }
20
+ .gui-own .card { position: absolute; left: 16px; bottom: 16px; width: 300px; padding: 12px 14px }
21
+ .gui-own .card h4 { margin: 0 0 6px; font-size: 18px; color: #fff }
22
+ .gui-own .card .bar { height: 10px; border-radius: 5px; background: #0b1118; border: 1px solid #27404f; overflow: hidden; margin: 6px 0 }
23
+ .gui-own .card .bar i { display: block; height: 100%; background: linear-gradient(90deg, #22c55e, #4fd1c5) }
24
+ .gui-own .card small { color: #93a8b8 }
25
+ .gui-own .orders { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; gap: 8px; padding: 8px }
26
+ .gui-own .orders button { pointer-events: auto; cursor: pointer; width: 74px; height: 64px; border-radius: 4px; border: 1px solid #2c5a66;
27
+ background: #0e1a24; color: #dff; font: 600 12px 'Trebuchet MS', sans-serif; display: grid; place-items: center; gap: 2px }
28
+ .gui-own .orders button em { font-style: normal; font-size: 22px; color: #4fd1c5 }
29
+ .gui-own .orders button:hover { border-color: #4fd1c5; background: #12303a }
30
+ .gui-own .log { position: absolute; right: 16px; bottom: 16px; width: 260px; max-height: 180px; overflow: hidden; padding: 10px 12px;
31
+ display: flex; flex-direction: column-reverse; gap: 3px; font-size: 12px }
32
+ .gui-own .log div { color: #b9cad6 } .gui-own .log div.foe { color: #ffb4a6 }
33
+ `
34
+
35
+ /** The card's words for what is selected: the first one's name and health, and how many more */
36
+ function cardOf(client: GameClientApi): string {
37
+ const selection = client.ui.selection()
38
+ if (!selection.length) return '<h4>Nothing selected</h4><small>Click a unit, or drag a box round several</small>'
39
+ const first = selection[0]
40
+ const share = first.health / Math.max(1, first.maxHealth)
41
+ const stats = first.stats
42
+ .slice(0, 3)
43
+ .map((line) => `${line.label} ${line.value}`)
44
+ .join(' · ')
45
+ return `<h4>${first.name}${selection.length > 1 ? ` <small>and ${selection.length - 1} more</small>` : ''}</h4>
46
+ <div class="bar"><i style="width:${share * 100}%"></i></div>
47
+ <small>${Math.round(first.health)} / ${Math.round(first.maxHealth)} health${stats ? ` · ${stats}` : ''}</small>`
48
+ }
49
+
50
+ /** Hides the client's HUD and puts the game's own up; gives back what takes it down and shows the client's again */
51
+ export function ownInterface(client: GameClientApi): () => void {
52
+ const showHud = client.hideHud()
53
+ const root = document.createElement('div')
54
+ root.className = 'gui-own'
55
+ root.innerHTML = `<style>${STYLE}</style>
56
+ <div class="top glass"><b>ELMBROOK GARRISON</b>
57
+ <span>Yours <span class="num ours" data-ours>0</span></span>
58
+ <span>Theirs <span class="num theirs" data-theirs>0</span></span>
59
+ <span data-time>0:00</span></div>
60
+ <div class="card glass" data-card></div>
61
+ <div class="orders glass" data-orders></div>
62
+ <div class="log glass" data-log><div>The armies stand ready.</div></div>`
63
+ const removeLayer = client.addLayer(root, { under: 'hud' })
64
+ const $ = (name: string) => root.querySelector<HTMLElement>(`[data-${name}]`)!
65
+
66
+ /** The selected units of the player's own */
67
+ const mine = () =>
68
+ client.ui
69
+ .selection()
70
+ .filter((item) => item.own)
71
+ .map((item) => item.id)
72
+ const orders: [string, string, () => void][] = [
73
+ ['⚔', 'Charge', () => client.session.send(charge.make({}))],
74
+ ['■', 'Stop', () => client.session.send({ type: 'order', entities: mine(), order: { type: 'stop' } })],
75
+ ['⛨', 'Hold', () => client.session.send({ type: 'order', entities: mine(), order: { type: 'hold' } })],
76
+ [
77
+ '✺',
78
+ 'Gather',
79
+ () => {
80
+ const pose = client.camera.pose()
81
+ client.session.send(gather.make({ at: { x: pose.x, z: pose.z } }))
82
+ }
83
+ ],
84
+ ['↺', 'Again', () => client.session.send(resetCamp.make({}))]
85
+ ]
86
+ for (const [glyph, label, run] of orders) {
87
+ const button = document.createElement('button')
88
+ button.type = 'button'
89
+ button.innerHTML = `<em>${glyph}</em>${label}`
90
+ button.onclick = run
91
+ $('orders').append(button)
92
+ }
93
+
94
+ // the selection's card, drawn again as the HUD's state changes
95
+ const stopCard = client.ui.watch(
96
+ () => cardOf(client),
97
+ (words) => ($('card').innerHTML = words),
98
+ { immediate: true }
99
+ )
100
+
101
+ // the numbers, the clock and the log, from the snapshot
102
+ let known = new Map<number, { owner: number; name: string }>()
103
+ const tick = () => {
104
+ const { entities, tick } = client.session.snapshot()
105
+ const alive = entities.filter((each) => each.health > 0)
106
+ $('ours').textContent = String(alive.filter((each) => each.owner === PLAYER).length)
107
+ $('theirs').textContent = String(alive.filter((each) => each.owner === ENEMY).length)
108
+ const seconds = Math.floor(tick / client.session.tickRate)
109
+ $('time').textContent = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`
110
+ const now = new Map(alive.map((each) => [each.id, { owner: each.owner, name: client.session.registry.unit(each.type)?.name ?? each.type }]))
111
+ for (const [id, was] of known) {
112
+ if (now.has(id)) continue
113
+ const line = document.createElement('div')
114
+ line.className = was.owner === PLAYER ? '' : 'foe'
115
+ line.textContent = `${was.owner === PLAYER ? 'Your' : 'An enemy'} ${was.name.toLowerCase()} fell.`
116
+ $('log').prepend(line)
117
+ while ($('log').children.length > 9) $('log').lastElementChild!.remove()
118
+ }
119
+ known = now
120
+ }
121
+ tick()
122
+ const timer = setInterval(tick, 300)
123
+ return () => {
124
+ clearInterval(timer)
125
+ stopCard()
126
+ removeLayer()
127
+ showHud()
128
+ }
129
+ }